@forwardimpact/libeval 0.1.53 → 0.1.55
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/fit-trace.js +3 -2
- package/package.json +1 -1
- package/src/commands/trace.js +1 -2
- package/src/index.js +1 -0
- package/src/profile-prompt.js +130 -48
- package/src/trace-github.js +46 -28
package/bin/fit-trace.js
CHANGED
|
@@ -41,7 +41,7 @@ const definition = {
|
|
|
41
41
|
argsUsage: "[pattern]",
|
|
42
42
|
handler: runRunsCommand,
|
|
43
43
|
description:
|
|
44
|
-
"List recent GitHub Actions workflow runs (default pattern: agent)",
|
|
44
|
+
"List recent GitHub Actions workflow runs (default pattern: kata|agent)",
|
|
45
45
|
options: {
|
|
46
46
|
lookback: {
|
|
47
47
|
type: "string",
|
|
@@ -59,7 +59,8 @@ const definition = {
|
|
|
59
59
|
args: ["run-id"],
|
|
60
60
|
argsUsage: "<run-id>",
|
|
61
61
|
handler: runDownloadCommand,
|
|
62
|
-
description:
|
|
62
|
+
description:
|
|
63
|
+
"Download trace artifact and convert to structured JSON; pass --artifact to pick one when a matrix workflow emits multiple `trace--*` artifacts",
|
|
63
64
|
options: {
|
|
64
65
|
dir: { type: "string", description: "Output directory" },
|
|
65
66
|
artifact: { type: "string", description: "Artifact name override" },
|
package/package.json
CHANGED
package/src/commands/trace.js
CHANGED
|
@@ -25,9 +25,8 @@ export async function runRunsCommand(ctx) {
|
|
|
25
25
|
repo: ctx.options.repo,
|
|
26
26
|
runtime,
|
|
27
27
|
});
|
|
28
|
-
const pattern = ctx.args.pattern ?? "agent";
|
|
29
28
|
const lookback = ctx.options.lookback ?? "7d";
|
|
30
|
-
const runs = await gh.listRuns({ pattern, lookback });
|
|
29
|
+
const runs = await gh.listRuns({ pattern: ctx.args.pattern, lookback });
|
|
31
30
|
writeJSON(runtime, runs, ctx.options);
|
|
32
31
|
return { ok: true };
|
|
33
32
|
}
|
package/src/index.js
CHANGED
package/src/profile-prompt.js
CHANGED
|
@@ -13,12 +13,26 @@
|
|
|
13
13
|
* </session_protocol>
|
|
14
14
|
*
|
|
15
15
|
* The two tags are siblings joined by a blank line — neither nests inside
|
|
16
|
-
* the other. A section appears only when its content is present.
|
|
17
|
-
* system-prompt amendment is folded into the protocol trailer before
|
|
18
|
-
* wrapping, so it lands transparently inside `<session_protocol>`. The tag
|
|
16
|
+
* the other. A section appears only when its content is present. The tag
|
|
19
17
|
* convention lives entirely here: profile `.md` files and trailer constants
|
|
20
18
|
* carry no tags.
|
|
21
19
|
*
|
|
20
|
+
* The `<session_protocol>` body is assembled from up to three fragments, in
|
|
21
|
+
* order of decreasing generality:
|
|
22
|
+
*
|
|
23
|
+
* 1. the role-invariant orchestration trailer (libeval-owned);
|
|
24
|
+
* 2. the profile's own hoisted `## Session Protocol` section, if present;
|
|
25
|
+
* 3. a run-specific amendment, if supplied.
|
|
26
|
+
*
|
|
27
|
+
* Fragment 2 is the convention-based hoist: a profile may carry a level-2
|
|
28
|
+
* `## Session Protocol` markdown heading whose body is the role's work
|
|
29
|
+
* routine. When present, that section is lifted out of `<agent_profile>` and
|
|
30
|
+
* folded into `<session_protocol>` next to the orchestration mechanics, so
|
|
31
|
+
* the harness comms protocol and the role's work routine read as one
|
|
32
|
+
* coherent block. The heading line itself is dropped — the tag already names
|
|
33
|
+
* the section. Profiles with no such heading are unaffected (the entire body
|
|
34
|
+
* stays in `<agent_profile>`).
|
|
35
|
+
*
|
|
22
36
|
* Helpers:
|
|
23
37
|
*
|
|
24
38
|
* - `composeProfilePrompt(name, opts)` — profile + `claude_code` preset.
|
|
@@ -28,9 +42,9 @@
|
|
|
28
42
|
* roles (supervisor, facilitator, discuss lead) that should only see
|
|
29
43
|
* the orchestration instructions and optionally a profile body.
|
|
30
44
|
*
|
|
31
|
-
* - `composeSystemPrompt(opts)` — unified entry point.
|
|
32
|
-
* the protocol section, then delegates to one
|
|
33
|
-
* `opts.role`.
|
|
45
|
+
* - `composeSystemPrompt(opts)` — unified entry point. Threads `amend` into
|
|
46
|
+
* the protocol section as the run-specific fragment, then delegates to one
|
|
47
|
+
* of the above based on `opts.role`.
|
|
34
48
|
*/
|
|
35
49
|
|
|
36
50
|
import { join } from "node:path";
|
|
@@ -39,6 +53,17 @@ import { join } from "node:path";
|
|
|
39
53
|
const AGENT_PROFILE_TAG = "agent_profile";
|
|
40
54
|
const SESSION_PROTOCOL_TAG = "session_protocol";
|
|
41
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A level-2 heading that names the profile's hoisted session-protocol
|
|
58
|
+
* section. Case-insensitive, tolerant of trailing whitespace, but the level
|
|
59
|
+
* is fixed at two `#` so a `### Session Protocol` subsection does not trip
|
|
60
|
+
* the hoist.
|
|
61
|
+
*/
|
|
62
|
+
const SESSION_PROTOCOL_HEADING = /^##[ \t]+session protocol[ \t]*$/i;
|
|
63
|
+
|
|
64
|
+
/** A level-1 or level-2 heading — the boundary that ends a hoisted section. */
|
|
65
|
+
const SECTION_BOUNDARY = /^#{1,2}[ \t]+\S/;
|
|
66
|
+
|
|
42
67
|
/** Wrap content in a semantic section tag, each on its own line. */
|
|
43
68
|
function wrapSection(tag, content) {
|
|
44
69
|
return `<${tag}>\n${content}\n</${tag}>`;
|
|
@@ -46,86 +71,148 @@ function wrapSection(tag, content) {
|
|
|
46
71
|
|
|
47
72
|
/**
|
|
48
73
|
* Assemble the parallel `<agent_profile>` / `<session_protocol>` sections.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
74
|
+
* The profile section is emitted only when `body` is non-empty. The protocol
|
|
75
|
+
* section is built by joining its fragments (in the order given) with a
|
|
76
|
+
* blank-line separator, dropping any that are empty, and is emitted only
|
|
77
|
+
* when at least one fragment survives. The two tags are siblings joined by a
|
|
78
|
+
* blank line and never nest.
|
|
51
79
|
*
|
|
52
80
|
* @param {object} parts
|
|
53
|
-
* @param {string} [parts.body] - Profile body,
|
|
54
|
-
*
|
|
55
|
-
*
|
|
81
|
+
* @param {string} [parts.body] - Profile body, frontmatter-stripped and with
|
|
82
|
+
* any `## Session Protocol` section already hoisted out.
|
|
83
|
+
* @param {Array<string | undefined>} [parts.protocolParts] - Ordered session
|
|
84
|
+
* protocol fragments: trailer, hoisted profile section, run amendment.
|
|
56
85
|
* @returns {string}
|
|
57
86
|
*/
|
|
58
|
-
function assembleSections({ body,
|
|
87
|
+
function assembleSections({ body, protocolParts = [] }) {
|
|
59
88
|
const sections = [];
|
|
60
89
|
if (body) sections.push(wrapSection(AGENT_PROFILE_TAG, body));
|
|
90
|
+
const protocol = protocolParts.filter(Boolean).join("\n\n");
|
|
61
91
|
if (protocol) sections.push(wrapSection(SESSION_PROTOCOL_TAG, protocol));
|
|
62
92
|
return sections.join("\n\n");
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
96
|
+
* Split a frontmatter-stripped profile body into its persona and an optional
|
|
97
|
+
* hoisted `## Session Protocol` section. The section runs from its heading to
|
|
98
|
+
* the next level-1/level-2 heading (or end of body); the heading line is
|
|
99
|
+
* dropped. Anything before and after the section is rejoined into `persona`.
|
|
100
|
+
* When the body carries no `## Session Protocol` heading, the whole body is
|
|
101
|
+
* returned as `persona` and `protocol` is `undefined`.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} body - Frontmatter-stripped, trimmed profile body.
|
|
104
|
+
* @returns {{ persona: string, protocol: string | undefined }}
|
|
105
|
+
*/
|
|
106
|
+
function splitSessionProtocol(body) {
|
|
107
|
+
const lines = body.split("\n");
|
|
108
|
+
const start = lines.findIndex((line) => SESSION_PROTOCOL_HEADING.test(line));
|
|
109
|
+
if (start === -1) return { persona: body, protocol: undefined };
|
|
110
|
+
|
|
111
|
+
let end = lines.length;
|
|
112
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
113
|
+
if (SECTION_BOUNDARY.test(lines[i])) {
|
|
114
|
+
end = i;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const protocol = lines
|
|
120
|
+
.slice(start + 1, end)
|
|
121
|
+
.join("\n")
|
|
122
|
+
.trim();
|
|
123
|
+
const before = lines.slice(0, start).join("\n").trim();
|
|
124
|
+
const after = lines.slice(end).join("\n").trim();
|
|
125
|
+
const persona = [before, after].filter(Boolean).join("\n\n");
|
|
126
|
+
return { persona, protocol: protocol || undefined };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Read a profile `.md`, strip its frontmatter, and split off any hoisted
|
|
131
|
+
* `## Session Protocol` section. Reads synchronously off the injected
|
|
132
|
+
* `runtime.fsSync` surface — this composer runs inside the synchronous
|
|
133
|
+
* SDK-option builders of the supervisor / facilitator / discusser / judge
|
|
134
|
+
* factories, so it cannot go async without an unbounded cascade.
|
|
71
135
|
*
|
|
72
136
|
* @param {string} name - Profile basename (no `.md` suffix)
|
|
73
137
|
* @param {string} profilesDir - Directory containing `<name>.md`
|
|
74
138
|
* @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
|
|
75
|
-
* @returns {string}
|
|
139
|
+
* @returns {{ persona: string, protocol: string | undefined }}
|
|
76
140
|
*/
|
|
77
|
-
function
|
|
141
|
+
function readProfileSections(name, profilesDir, runtime) {
|
|
78
142
|
const path = join(profilesDir, `${name}.md`);
|
|
79
143
|
const raw = runtime.fsSync.readFileSync(path, "utf8");
|
|
80
|
-
return stripFrontmatter(raw).trim();
|
|
144
|
+
return splitSessionProtocol(stripFrontmatter(raw).trim());
|
|
81
145
|
}
|
|
82
146
|
|
|
83
147
|
/**
|
|
84
148
|
* Compose a `claude_code`-preset system prompt from a profile file. The
|
|
85
|
-
*
|
|
86
|
-
*
|
|
149
|
+
* persona is wrapped in `<agent_profile>`; the protocol trailer, the
|
|
150
|
+
* profile's hoisted `## Session Protocol` section, and any amendment are
|
|
151
|
+
* joined (in that order) into a sibling `<session_protocol>`.
|
|
87
152
|
*
|
|
88
153
|
* @param {string} name - Profile basename (no `.md` suffix)
|
|
89
154
|
* @param {object} opts
|
|
90
155
|
* @param {string} opts.profilesDir - Directory containing `<name>.md`
|
|
91
|
-
* @param {string} [opts.trailer] - Session protocol
|
|
92
|
-
* `<session_protocol>` section
|
|
156
|
+
* @param {string} [opts.trailer] - Session protocol orchestration mechanics,
|
|
157
|
+
* the first fragment of the `<session_protocol>` section.
|
|
158
|
+
* @param {string} [opts.amend] - Run-specific amendment, the last fragment of
|
|
159
|
+
* the `<session_protocol>` section.
|
|
93
160
|
* @param {import("@forwardimpact/libutil/runtime").Runtime} opts.runtime - Ambient collaborators; uses `fsSync.readFileSync`.
|
|
94
161
|
* @returns {{type: "preset", preset: "claude_code", append: string}}
|
|
95
162
|
*/
|
|
96
|
-
export function composeProfilePrompt(
|
|
97
|
-
|
|
163
|
+
export function composeProfilePrompt(
|
|
164
|
+
name,
|
|
165
|
+
{ profilesDir, trailer, amend, runtime },
|
|
166
|
+
) {
|
|
167
|
+
const { persona, protocol } = readProfileSections(name, profilesDir, runtime);
|
|
98
168
|
return {
|
|
99
169
|
type: "preset",
|
|
100
170
|
preset: "claude_code",
|
|
101
|
-
append: assembleSections({
|
|
171
|
+
append: assembleSections({
|
|
172
|
+
body: persona,
|
|
173
|
+
protocolParts: [trailer, protocol, amend],
|
|
174
|
+
}),
|
|
102
175
|
};
|
|
103
176
|
}
|
|
104
177
|
|
|
105
178
|
/**
|
|
106
179
|
* Compose a plain-string system prompt for a lead role (no Claude Code
|
|
107
|
-
* preset). The protocol trailer
|
|
108
|
-
*
|
|
180
|
+
* preset). The protocol trailer, an optional profile's hoisted
|
|
181
|
+
* `## Session Protocol` section, and any amendment are joined into
|
|
182
|
+
* `<session_protocol>`; an optional persona is wrapped in a sibling
|
|
183
|
+
* `<agent_profile>` before it.
|
|
109
184
|
*
|
|
110
185
|
* @param {object} opts
|
|
111
186
|
* @param {string} [opts.profile] - Profile basename (no `.md` suffix)
|
|
112
187
|
* @param {string} [opts.profilesDir] - Directory containing profile files
|
|
113
188
|
* @param {string} opts.trailer - Session protocol (orchestration instructions)
|
|
189
|
+
* @param {string} [opts.amend] - Run-specific amendment, the last fragment of
|
|
190
|
+
* the `<session_protocol>` section.
|
|
114
191
|
* @param {import("@forwardimpact/libutil/runtime").Runtime} opts.runtime - Ambient collaborators; uses `fsSync.readFileSync`.
|
|
115
192
|
* @returns {string}
|
|
116
193
|
*/
|
|
117
|
-
export function composeLeadPrompt({
|
|
194
|
+
export function composeLeadPrompt({
|
|
195
|
+
profile,
|
|
196
|
+
profilesDir,
|
|
197
|
+
trailer,
|
|
198
|
+
amend,
|
|
199
|
+
runtime,
|
|
200
|
+
}) {
|
|
118
201
|
if (!trailer) throw new Error("trailer is required");
|
|
119
|
-
const
|
|
120
|
-
?
|
|
121
|
-
: undefined;
|
|
122
|
-
return assembleSections({
|
|
202
|
+
const { persona, protocol } = profile
|
|
203
|
+
? readProfileSections(profile, profilesDir, runtime)
|
|
204
|
+
: { persona: undefined, protocol: undefined };
|
|
205
|
+
return assembleSections({
|
|
206
|
+
body: persona,
|
|
207
|
+
protocolParts: [trailer, protocol, amend],
|
|
208
|
+
});
|
|
123
209
|
}
|
|
124
210
|
|
|
125
211
|
/**
|
|
126
|
-
* Unified entry point for composing system prompts.
|
|
127
|
-
* amendment
|
|
128
|
-
*
|
|
212
|
+
* Unified entry point for composing system prompts. Threads an optional
|
|
213
|
+
* amendment through as the run-specific fragment of `<session_protocol>`
|
|
214
|
+
* (after the trailer and any hoisted profile section), then delegates by
|
|
215
|
+
* role.
|
|
129
216
|
*
|
|
130
217
|
* @param {object} opts
|
|
131
218
|
* @param {"lead"|"agent"} opts.role - `"lead"` produces a plain string;
|
|
@@ -133,8 +220,8 @@ export function composeLeadPrompt({ profile, profilesDir, trailer, runtime }) {
|
|
|
133
220
|
* @param {string} [opts.profile] - Profile basename
|
|
134
221
|
* @param {string} [opts.profilesDir]
|
|
135
222
|
* @param {string} opts.trailer - Session protocol (orchestration instructions)
|
|
136
|
-
* @param {string} [opts.amend] - Caller-supplied amendment,
|
|
137
|
-
* `<session_protocol
|
|
223
|
+
* @param {string} [opts.amend] - Caller-supplied amendment, the last fragment
|
|
224
|
+
* inside `<session_protocol>`, joined with a blank-line separator.
|
|
138
225
|
* @param {import("@forwardimpact/libutil/runtime").Runtime} opts.runtime - Ambient collaborators; uses `fsSync.readFileSync`.
|
|
139
226
|
* @returns {string | {type: "preset", preset: "claude_code", append: string}}
|
|
140
227
|
*/
|
|
@@ -147,26 +234,21 @@ export function composeSystemPrompt({
|
|
|
147
234
|
runtime,
|
|
148
235
|
}) {
|
|
149
236
|
if (!trailer) throw new Error("trailer is required");
|
|
150
|
-
const protocol = amend ? `${trailer}\n\n${amend}` : trailer;
|
|
151
237
|
if (role === "lead") {
|
|
152
|
-
return composeLeadPrompt({
|
|
153
|
-
profile,
|
|
154
|
-
profilesDir,
|
|
155
|
-
trailer: protocol,
|
|
156
|
-
runtime,
|
|
157
|
-
});
|
|
238
|
+
return composeLeadPrompt({ profile, profilesDir, trailer, amend, runtime });
|
|
158
239
|
}
|
|
159
240
|
if (profile) {
|
|
160
241
|
return composeProfilePrompt(profile, {
|
|
161
242
|
profilesDir,
|
|
162
|
-
trailer
|
|
243
|
+
trailer,
|
|
244
|
+
amend,
|
|
163
245
|
runtime,
|
|
164
246
|
});
|
|
165
247
|
}
|
|
166
248
|
return {
|
|
167
249
|
type: "preset",
|
|
168
250
|
preset: "claude_code",
|
|
169
|
-
append: assembleSections({
|
|
251
|
+
append: assembleSections({ protocolParts: [trailer, amend] }),
|
|
170
252
|
};
|
|
171
253
|
}
|
|
172
254
|
|
package/src/trace-github.js
CHANGED
|
@@ -31,13 +31,13 @@ export class TraceGitHub {
|
|
|
31
31
|
* List recent workflow runs, optionally filtered by name pattern.
|
|
32
32
|
*
|
|
33
33
|
* @param {object} [opts]
|
|
34
|
-
* @param {string} [opts.pattern] - Case-insensitive
|
|
34
|
+
* @param {string} [opts.pattern] - Case-insensitive regex to match workflow name (default: "kata|agent" — covers `Kata: Shift`, `Kata: Dispatch`, and any `agent`-named workflow)
|
|
35
35
|
* @param {number} [opts.limit=50] - Max runs to return from GitHub API
|
|
36
36
|
* @param {string} [opts.lookback="7d"] - How far back to search (e.g. "7d", "24h", "2w")
|
|
37
37
|
* @returns {Promise<object[]>} Array of {workflow, runId, status, conclusion, createdAt, branch, url}
|
|
38
38
|
*/
|
|
39
39
|
async listRuns(opts = {}) {
|
|
40
|
-
const { pattern = "agent", limit = 50, lookback = "7d" } = opts;
|
|
40
|
+
const { pattern = "kata|agent", limit = 50, lookback = "7d" } = opts;
|
|
41
41
|
const cutoff = parseLookback(lookback, this.runtime.clock.now());
|
|
42
42
|
|
|
43
43
|
const params = new URLSearchParams({
|
|
@@ -68,10 +68,10 @@ export class TraceGitHub {
|
|
|
68
68
|
/**
|
|
69
69
|
* Download a trace artifact from a workflow run and extract it.
|
|
70
70
|
*
|
|
71
|
-
* When `opts.name` is set, looks up that exact artifact. Otherwise picks
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* When `opts.name` is set, looks up that exact artifact. Otherwise picks
|
|
72
|
+
* the single `trace--*` artifact if exactly one exists, or throws with a
|
|
73
|
+
* disambiguation list when matrix workflows emit multiple per-participant
|
|
74
|
+
* artifacts (see {@link pickTraceArtifact}).
|
|
75
75
|
*
|
|
76
76
|
* @param {number|string} runId
|
|
77
77
|
* @param {object} [opts]
|
|
@@ -88,28 +88,7 @@ export class TraceGitHub {
|
|
|
88
88
|
const url = `${API}/repos/${this.owner}/${this.repo}/actions/runs/${runId}/artifacts`;
|
|
89
89
|
const data = await this.#get(url);
|
|
90
90
|
const artifacts = data.artifacts ?? [];
|
|
91
|
-
|
|
92
|
-
// Find the trace artifact.
|
|
93
|
-
let artifact = null;
|
|
94
|
-
if (opts.name) {
|
|
95
|
-
artifact = artifacts.find((a) => a.name === opts.name);
|
|
96
|
-
} else {
|
|
97
|
-
const traceArtifacts = artifacts.filter((a) =>
|
|
98
|
-
a.name.startsWith("trace--"),
|
|
99
|
-
);
|
|
100
|
-
artifact =
|
|
101
|
-
traceArtifacts.find((a) => a.name.endsWith(".raw")) ??
|
|
102
|
-
traceArtifacts.find((a) => a.name.endsWith(".agent")) ??
|
|
103
|
-
traceArtifacts[0] ??
|
|
104
|
-
null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!artifact) {
|
|
108
|
-
const available = artifacts.map((a) => a.name).join(", ");
|
|
109
|
-
throw new Error(
|
|
110
|
-
`No trace artifact found for run ${runId}. Available: ${available || "none"}`,
|
|
111
|
-
);
|
|
112
|
-
}
|
|
91
|
+
const artifact = pickTraceArtifact(artifacts, opts.name, runId);
|
|
113
92
|
|
|
114
93
|
// Download the zip.
|
|
115
94
|
const zipPath = path.join(dir, `${artifact.name}.zip`);
|
|
@@ -172,6 +151,45 @@ export class TraceGitHub {
|
|
|
172
151
|
}
|
|
173
152
|
}
|
|
174
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Pick the trace artifact to download from a workflow run's artifact list.
|
|
156
|
+
*
|
|
157
|
+
* When `name` is given, returns the exact match or throws with the available
|
|
158
|
+
* names. When `name` is omitted, returns the only `trace--*` artifact if
|
|
159
|
+
* there is exactly one; if there are multiple (matrix workflows like
|
|
160
|
+
* `kata-shift.yml` emit one `trace--<participant>` per cell), throws and
|
|
161
|
+
* lists them so the caller can pass `--name` to disambiguate.
|
|
162
|
+
*
|
|
163
|
+
* @param {Array<{name: string}>} artifacts - Artifact list from the GitHub API.
|
|
164
|
+
* @param {string} [name] - Exact artifact name to match.
|
|
165
|
+
* @param {number|string} [runId] - Run id for error messages.
|
|
166
|
+
* @returns {{name: string}} The selected artifact.
|
|
167
|
+
*/
|
|
168
|
+
export function pickTraceArtifact(artifacts, name, runId) {
|
|
169
|
+
const runRef = runId == null ? "" : ` for run ${runId}`;
|
|
170
|
+
if (name) {
|
|
171
|
+
const found = artifacts.find((a) => a.name === name);
|
|
172
|
+
if (found) return found;
|
|
173
|
+
const available = artifacts.map((a) => a.name).join(", ");
|
|
174
|
+
throw new Error(
|
|
175
|
+
`No artifact named "${name}"${runRef}. Available: ${available || "none"}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const traceArtifacts = artifacts.filter((a) => a.name.startsWith("trace--"));
|
|
180
|
+
if (traceArtifacts.length === 1) return traceArtifacts[0];
|
|
181
|
+
if (traceArtifacts.length === 0) {
|
|
182
|
+
const available = artifacts.map((a) => a.name).join(", ");
|
|
183
|
+
throw new Error(
|
|
184
|
+
`No trace artifact found${runRef}. Available: ${available || "none"}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const names = traceArtifacts.map((a) => a.name).join(", ");
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Multiple trace artifacts found${runRef}: ${names}. Pass --name to choose one.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
175
193
|
/**
|
|
176
194
|
* Parse a lookback duration string into an ISO date string.
|
|
177
195
|
* Supports: Nd (days), Nh (hours), Nw (weeks).
|