@elmundi/ship-cli 0.11.2 → 0.12.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/README.md +255 -22
- package/bin/shipctl.mjs +10 -7
- package/lib/bootstrap/render.mjs +49 -0
- package/lib/commands/callback.mjs +457 -17
- package/lib/commands/config.mjs +1 -1
- package/lib/commands/docs.mjs +4 -4
- package/lib/commands/help.mjs +137 -77
- package/lib/commands/kickoff.mjs +3 -3
- package/lib/commands/knowledge.mjs +211 -17
- package/lib/commands/lanes.mjs +36 -11
- package/lib/commands/manifest-catalog.mjs +5 -5
- package/lib/commands/patterns.mjs +5 -5
- package/lib/commands/run.mjs +329 -89
- package/lib/commands/search.mjs +2 -2
- package/lib/commands/sync.mjs +83 -8
- package/lib/commands/trigger.mjs +196 -0
- package/lib/config/migrate.mjs +13 -5
- package/lib/config/schema.mjs +253 -2
- package/lib/state/idempotency.mjs +1 -1
- package/lib/templates.mjs +3 -3
- package/package.json +2 -2
package/lib/commands/run.mjs
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* `shipctl run` — single entry-point for executing a Ship lane (RFC-0007).
|
|
3
3
|
*
|
|
4
4
|
* Today's scope (Phase 1):
|
|
5
|
-
* - `kind=once` lanes run
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* that makes those kinds execute.
|
|
5
|
+
* - `kind=once` lanes run with local idempotency markers.
|
|
6
|
+
* - `kind=event` and `kind=schedule` lanes execute when Ship's trigger
|
|
7
|
+
* router says they are due; router-side audit state prevents duplicate
|
|
8
|
+
* runs for the same schedule window.
|
|
10
9
|
*
|
|
11
10
|
* The command intentionally does not fork an agent subprocess. The
|
|
12
11
|
* reusable workflow pipes shipctl's stdout into the customer's agent
|
|
@@ -25,7 +24,13 @@ import fs from "node:fs";
|
|
|
25
24
|
import path from "node:path";
|
|
26
25
|
|
|
27
26
|
import { readConfig, findShipRoot } from "../config/io.mjs";
|
|
28
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
validateConfig,
|
|
29
|
+
CONFIG_SCHEMA_VERSION,
|
|
30
|
+
lanePatterns,
|
|
31
|
+
laneFanout,
|
|
32
|
+
LANE_FANOUT_MODES,
|
|
33
|
+
} from "../config/schema.mjs";
|
|
29
34
|
import { fetchArtifact } from "../http.mjs";
|
|
30
35
|
import { resolveShipRepoRootForCatalog } from "../find-ship-root.mjs";
|
|
31
36
|
import { readArtifactFile } from "../artifacts/fs-index.mjs";
|
|
@@ -41,16 +46,39 @@ const EXIT_IDEMPOTENCY = 4;
|
|
|
41
46
|
const VALID_TRIGGERS = new Set(["event", "schedule", "manual", "once"]);
|
|
42
47
|
|
|
43
48
|
function printHelp() {
|
|
44
|
-
console.log(`shipctl run — execute a Ship lane
|
|
49
|
+
console.log(`shipctl run — execute a Ship lane (Automation in the operator console).
|
|
50
|
+
|
|
51
|
+
WHAT THIS COMMAND IS FOR
|
|
52
|
+
shipctl run is the **one-shot dispatch** entry point. It resolves a
|
|
53
|
+
lane from .ship/config.yml, fetches its pattern body, checks
|
|
54
|
+
idempotency, and emits the prompt for an agent to consume. Behaviour
|
|
55
|
+
by lane kind:
|
|
56
|
+
- kind: once — executed fully here, locally.
|
|
57
|
+
- kind: lane / event / — recognised but NOT executed locally;
|
|
58
|
+
schedule those run via the workspace's GitHub
|
|
59
|
+
Actions runner using the reusable
|
|
60
|
+
.github/workflows/run-agent.yml. shipctl
|
|
61
|
+
run exits 0 with a no-op summary so CI
|
|
62
|
+
wrappers can wire them safely.
|
|
45
63
|
|
|
46
64
|
USAGE
|
|
47
|
-
shipctl run --lane <id> [--
|
|
65
|
+
shipctl run --lane <id> [--pattern <id>] [--fanout <matrix|sequential|concurrent>]
|
|
66
|
+
[--trigger <event|schedule|manual|once>]
|
|
48
67
|
[--dry-run] [--offline]
|
|
49
68
|
[--ship-run-id <uuid>] [--ship-callback-url <url>] [--ship-run-token <jwt>]
|
|
50
69
|
[--cwd <dir>] [--json]
|
|
51
70
|
|
|
52
71
|
FLAGS
|
|
53
72
|
--lane <id> Lane id declared in .ship/config.yml. Required.
|
|
73
|
+
--pattern <id> For multi-pattern lanes: run only this pattern. This
|
|
74
|
+
is the per-entry call issued by the matrix workflow
|
|
75
|
+
(one matrix job per pattern). Must be one of the
|
|
76
|
+
lane's declared patterns.
|
|
77
|
+
--fanout <mode> Override the lane's configured fan-out for this run
|
|
78
|
+
(matrix|sequential|concurrent). Meaningful only
|
|
79
|
+
when the lane has ≥2 patterns and --pattern is not
|
|
80
|
+
set. Matrix mode without --pattern is rejected;
|
|
81
|
+
it requires a driving workflow.
|
|
54
82
|
--trigger <kind> Force the trigger context (event|schedule|manual|once).
|
|
55
83
|
If omitted, inferred from GITHUB_EVENT_NAME / SHIP_RUN_TRIGGER.
|
|
56
84
|
--dry-run Print the plan without touching idempotency markers or callback.
|
|
@@ -150,49 +178,107 @@ export async function runCommand(ctx, rest) {
|
|
|
150
178
|
process.exit(EXIT_OK);
|
|
151
179
|
}
|
|
152
180
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
// RFC-0008 C3.1/C3.2: resolve the list of patterns that this invocation
|
|
182
|
+
// should execute.
|
|
183
|
+
//
|
|
184
|
+
// --pattern <id> → run only that pattern (the per-entry call
|
|
185
|
+
// issued by the matrix workflow). The pattern
|
|
186
|
+
// must be one of the lane's declared patterns,
|
|
187
|
+
// otherwise we refuse so typos don't silently
|
|
188
|
+
// execute an unrelated pattern.
|
|
189
|
+
// (none) → run every pattern the lane declares, using
|
|
190
|
+
// the lane's fan-out mode. Matrix mode without
|
|
191
|
+
// --pattern is rejected because it requires a
|
|
192
|
+
// driving workflow (see run-agent.yml).
|
|
193
|
+
const allPatterns = lanePatterns(lane);
|
|
194
|
+
if (allPatterns.length === 0) {
|
|
195
|
+
die(EXIT_USAGE, `lane ${JSON.stringify(args.lane)} declares no patterns.`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const effectiveFanout = args.fanout || laneFanout(lane);
|
|
199
|
+
let patternsToRun;
|
|
200
|
+
let runMode; // ``single`` | ``sequential`` | ``concurrent``
|
|
201
|
+
if (args.pattern) {
|
|
202
|
+
if (!allPatterns.includes(args.pattern)) {
|
|
203
|
+
die(
|
|
204
|
+
EXIT_USAGE,
|
|
205
|
+
`--pattern=${JSON.stringify(args.pattern)} is not declared on lane ${JSON.stringify(args.lane)}. ` +
|
|
206
|
+
`Known patterns: ${allPatterns.join(", ")}.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
patternsToRun = [args.pattern];
|
|
210
|
+
runMode = "single";
|
|
211
|
+
} else if (allPatterns.length === 1) {
|
|
212
|
+
patternsToRun = allPatterns;
|
|
213
|
+
runMode = "single";
|
|
214
|
+
} else if (effectiveFanout === "matrix") {
|
|
215
|
+
die(
|
|
216
|
+
EXIT_USAGE,
|
|
217
|
+
`lane ${JSON.stringify(args.lane)} has fanout=matrix and ${allPatterns.length} patterns ` +
|
|
218
|
+
`but no --pattern was provided. Matrix mode dispatches one 'shipctl run --pattern <id>' per ` +
|
|
219
|
+
`pattern via the workflow (see run-agent.yml). To run them in-process instead, pass ` +
|
|
220
|
+
`--fanout sequential or --fanout concurrent.`,
|
|
221
|
+
);
|
|
222
|
+
} else {
|
|
223
|
+
patternsToRun = allPatterns;
|
|
224
|
+
runMode = effectiveFanout;
|
|
181
225
|
}
|
|
182
226
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
227
|
+
// Idempotency markers are lane-scoped (not per-pattern) so we read
|
|
228
|
+
// once up front; per-pattern decisions are derived from the
|
|
229
|
+
// concatenated pattern SHA set below so a change to any member of
|
|
230
|
+
// the list re-triggers the run (expected behaviour for audit lanes).
|
|
231
|
+
const idem = lane.kind === "once" ? lane.idempotency : null;
|
|
187
232
|
let marker = null;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
233
|
+
if (idem) {
|
|
234
|
+
try {
|
|
235
|
+
marker = readMarker(cwd, idem.key);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
await tryCallback(args, "fail", `idempotency read failed: ${err.message}`);
|
|
238
|
+
die(EXIT_IDEMPOTENCY, err instanceof Error ? err.message : String(err));
|
|
239
|
+
}
|
|
193
240
|
}
|
|
194
241
|
|
|
195
|
-
|
|
242
|
+
// Fetch every pattern body first so we can reject the whole run
|
|
243
|
+
// atomically if any one is unavailable — partial success is worse
|
|
244
|
+
// than a hard failure here (the caller can retry once the fetch
|
|
245
|
+
// error is cleared).
|
|
246
|
+
const fetchJobs = patternsToRun.map((patternId) =>
|
|
247
|
+
fetchPatternBody({
|
|
248
|
+
patternId,
|
|
249
|
+
patternVersion: lane.pattern_version || null,
|
|
250
|
+
offline: args.offline,
|
|
251
|
+
root,
|
|
252
|
+
ctx,
|
|
253
|
+
config,
|
|
254
|
+
}).then((result) => ({ patternId, result })),
|
|
255
|
+
);
|
|
256
|
+
// `sequential` vs `concurrent` only differ for future in-process agent
|
|
257
|
+
// invocation; today's CLI just emits the pattern bodies to stdout, so
|
|
258
|
+
// both modes fetch in parallel and print in declared order. We still
|
|
259
|
+
// record the requested mode on the summary so downstream consumers
|
|
260
|
+
// (and future work) can see the intent.
|
|
261
|
+
const fetched = await Promise.all(fetchJobs);
|
|
262
|
+
for (const { patternId, result } of fetched) {
|
|
263
|
+
if (!result.ok) {
|
|
264
|
+
die(EXIT_USAGE, `pattern ${patternId}: ${result.error}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const runs = fetched.map(({ patternId, result }) => ({
|
|
268
|
+
patternId,
|
|
269
|
+
body: result.body,
|
|
270
|
+
source: result.source,
|
|
271
|
+
sha256: sha256(result.body),
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
// Composite SHA over all pattern bodies. ``reset_on=version-change``
|
|
275
|
+
// fires when any member's body drifts — which is the correct
|
|
276
|
+
// semantics for a multi-pattern audit lane: if one role's playbook
|
|
277
|
+
// updates, we want the whole lane to re-run.
|
|
278
|
+
const compositeBody = runs.map((r) => `#${r.patternId}\n${r.body}`).join("\n---\n");
|
|
279
|
+
const decision = idem
|
|
280
|
+
? decideRun(marker, compositeBody, idem.reset_on || "version-change")
|
|
281
|
+
: { run: true, reason: "trigger-router-due", marker: null };
|
|
196
282
|
if (!decision.run) {
|
|
197
283
|
const summary = {
|
|
198
284
|
lane: args.lane,
|
|
@@ -201,20 +287,25 @@ export async function runCommand(ctx, rest) {
|
|
|
201
287
|
status: "noop",
|
|
202
288
|
reason: "already-done",
|
|
203
289
|
marker: decision.marker,
|
|
290
|
+
patterns: runs.map((r) => ({ id: r.patternId, sha256: r.sha256 })),
|
|
204
291
|
};
|
|
205
292
|
await tryCallback(
|
|
206
293
|
args,
|
|
207
294
|
"ok",
|
|
208
295
|
`lane ${args.lane}: already completed, no-op.`,
|
|
209
|
-
|
|
296
|
+
runMode === "single"
|
|
297
|
+
? { pattern_id: runs[0].patternId, pattern_sha256: runs[0].sha256, noop: true }
|
|
298
|
+
: { patterns: runs.map((r) => r.patternId), noop: true },
|
|
210
299
|
);
|
|
211
300
|
emitSummary(ctx, args, summary);
|
|
212
301
|
process.exit(EXIT_OK);
|
|
213
302
|
}
|
|
214
303
|
|
|
215
304
|
/* Dry-run stops here — no marker write, no callback, just print the
|
|
216
|
-
* plan. We still emit the pattern
|
|
217
|
-
* eyeball what the agent would receive.
|
|
305
|
+
* plan. We still emit the pattern bodies to stdout so operators can
|
|
306
|
+
* eyeball what the agent would receive. Multi-pattern runs print
|
|
307
|
+
* each body preceded by a ``# ship: pattern=<id>`` banner so the
|
|
308
|
+
* agent-side (or a human) can split them back apart. */
|
|
218
309
|
if (args.dryRun || ctx.dryRun) {
|
|
219
310
|
const summary = {
|
|
220
311
|
lane: args.lane,
|
|
@@ -222,65 +313,192 @@ export async function runCommand(ctx, rest) {
|
|
|
222
313
|
trigger: effectiveTrigger.trigger,
|
|
223
314
|
status: "dry-run",
|
|
224
315
|
reason: decision.reason,
|
|
225
|
-
|
|
316
|
+
mode: runMode,
|
|
317
|
+
patterns: runs.map((r) => ({ id: r.patternId, sha256: r.sha256, source: r.source })),
|
|
226
318
|
};
|
|
319
|
+
if (runs.length === 1) {
|
|
320
|
+
summary.pattern = { id: runs[0].patternId, sha256: runs[0].sha256, source: runs[0].source };
|
|
321
|
+
}
|
|
227
322
|
if (ctx.json || args.json) {
|
|
228
|
-
console.log(
|
|
323
|
+
console.log(
|
|
324
|
+
JSON.stringify(
|
|
325
|
+
{ ...summary, pattern_bodies: Object.fromEntries(runs.map((r) => [r.patternId, r.body])) },
|
|
326
|
+
null,
|
|
327
|
+
2,
|
|
328
|
+
),
|
|
329
|
+
);
|
|
229
330
|
} else {
|
|
230
|
-
console.error(
|
|
231
|
-
|
|
331
|
+
console.error(
|
|
332
|
+
`# ship: lane=${args.lane} kind=${lane.kind} trigger=${effectiveTrigger.trigger} mode=${runMode} (dry-run)`,
|
|
333
|
+
);
|
|
334
|
+
emitPatternBodies(runs, { json: false });
|
|
232
335
|
}
|
|
233
336
|
process.exit(EXIT_OK);
|
|
234
337
|
}
|
|
235
338
|
|
|
236
|
-
/* Emit the prompt for the agent to consume (same contract as
|
|
237
|
-
* `shipctl kickoff`). The reusable workflow pipes
|
|
238
|
-
* configured agent runtime
|
|
339
|
+
/* Emit the prompt(s) for the agent to consume (same contract as
|
|
340
|
+
* `shipctl kickoff`). The reusable workflow pipes stdout into the
|
|
341
|
+
* configured agent runtime; multi-pattern output is delimited by a
|
|
342
|
+
* banner line per pattern so consumers can split on it.
|
|
343
|
+
*
|
|
344
|
+
* Workspace policy injection (RFC-Workspace-policy): before any
|
|
345
|
+
* pattern body, fetch the workspace's prose-rule policies from the
|
|
346
|
+
* backend and prepend them as a markdown block. This makes the
|
|
347
|
+
* agent treat the policies as hard preamble — the same shape the
|
|
348
|
+
* Navigator chat injects into ``TopicService.assemble_messages``.
|
|
349
|
+
* Best-effort: a missing token, missing callback URL, or a network
|
|
350
|
+
* failure quietly skips the prepend so local / offline runs still
|
|
351
|
+
* work. */
|
|
239
352
|
if (!(ctx.json || args.json)) {
|
|
240
353
|
const provider = resolveAgentProvider(config, args.lane);
|
|
241
|
-
if (provider) console.error(`# ship: lane=${args.lane} agent.provider=${provider}`);
|
|
242
|
-
|
|
354
|
+
if (provider) console.error(`# ship: lane=${args.lane} agent.provider=${provider} mode=${runMode}`);
|
|
355
|
+
const preamble = await fetchPoliciesPreamble(args);
|
|
356
|
+
if (preamble) emitPoliciesPreamble(preamble);
|
|
357
|
+
emitPatternBodies(runs, { json: false });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (idem) {
|
|
361
|
+
try {
|
|
362
|
+
writeMarker(cwd, idem.key, {
|
|
363
|
+
lane: args.lane,
|
|
364
|
+
pattern_id: runs[0].patternId,
|
|
365
|
+
pattern_sha256: sha256(compositeBody),
|
|
366
|
+
pattern_version: lane.pattern_version || null,
|
|
367
|
+
patterns: runs.map((r) => ({ id: r.patternId, sha256: r.sha256 })),
|
|
368
|
+
});
|
|
369
|
+
} catch (err) {
|
|
370
|
+
await tryCallback(args, "fail", `idempotency write failed: ${err.message}`);
|
|
371
|
+
die(EXIT_IDEMPOTENCY, err instanceof Error ? err.message : String(err));
|
|
372
|
+
}
|
|
243
373
|
}
|
|
244
374
|
|
|
245
|
-
|
|
246
|
-
|
|
375
|
+
const callbackMetrics = runMode === "single"
|
|
376
|
+
? { pattern_id: runs[0].patternId, pattern_sha256: runs[0].sha256 }
|
|
377
|
+
: {
|
|
378
|
+
pattern_id: runs[0].patternId,
|
|
379
|
+
pattern_sha256: runs[0].sha256,
|
|
380
|
+
patterns: runs.map((r) => r.patternId).join(","),
|
|
381
|
+
};
|
|
382
|
+
const callbackSummary = runMode === "single"
|
|
383
|
+
? `lane ${args.lane} completed (pattern ${runs[0].patternId}@${runs[0].sha256.slice(0, 8)}).`
|
|
384
|
+
: `lane ${args.lane} completed (${runs.length} patterns, mode=${runMode}).`;
|
|
385
|
+
const callbackResult = await tryCallback(args, "ok", callbackSummary, callbackMetrics);
|
|
386
|
+
|
|
387
|
+
if (ctx.json || args.json) {
|
|
388
|
+
// For single-pattern runs we keep the legacy ``pattern: {…}`` key
|
|
389
|
+
// alongside the new ``patterns: […]`` list so existing consumers
|
|
390
|
+
// (and tests) don't break when they upgrade shipctl before
|
|
391
|
+
// starting to declare multi-pattern lanes.
|
|
392
|
+
const summaryPayload = {
|
|
247
393
|
lane: args.lane,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
394
|
+
kind: lane.kind,
|
|
395
|
+
trigger: effectiveTrigger.trigger,
|
|
396
|
+
status: "completed",
|
|
397
|
+
mode: runMode,
|
|
398
|
+
patterns: runs.map((r) => ({ id: r.patternId, sha256: r.sha256, source: r.source })),
|
|
399
|
+
callback: callbackResult,
|
|
400
|
+
};
|
|
401
|
+
if (runs.length === 1) {
|
|
402
|
+
summaryPayload.pattern = { id: runs[0].patternId, sha256: runs[0].sha256, source: runs[0].source };
|
|
403
|
+
}
|
|
404
|
+
console.log(JSON.stringify(summaryPayload, null, 2));
|
|
255
405
|
}
|
|
256
406
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
"ok",
|
|
260
|
-
`lane ${args.lane} completed (pattern ${patternId}@${patternSha.slice(0, 8)}).`,
|
|
261
|
-
{ pattern_id: patternId, pattern_sha256: patternSha },
|
|
262
|
-
);
|
|
407
|
+
process.exit(callbackResult.ok === false ? EXIT_CALLBACK : EXIT_OK);
|
|
408
|
+
}
|
|
263
409
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
);
|
|
410
|
+
/**
|
|
411
|
+
* Stream pattern bodies to stdout. For single-pattern runs we write
|
|
412
|
+
* the body as-is (identical byte output to the pre-multi-pattern
|
|
413
|
+
* behaviour, keeping the test harness stable). For multi-pattern
|
|
414
|
+
* runs we precede each body with a ``# ship: pattern=<id>`` banner so
|
|
415
|
+
* downstream consumers can re-split the stream.
|
|
416
|
+
*/
|
|
417
|
+
function emitPatternBodies(runs, _opts) {
|
|
418
|
+
if (runs.length === 1) {
|
|
419
|
+
const body = runs[0].body;
|
|
420
|
+
process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
for (const r of runs) {
|
|
424
|
+
process.stdout.write(`# ship: pattern=${r.patternId} sha256=${r.sha256}\n`);
|
|
425
|
+
const body = r.body;
|
|
426
|
+
process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
|
|
279
427
|
}
|
|
428
|
+
}
|
|
280
429
|
|
|
281
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Print the workspace-policies preamble once at the top of stdout,
|
|
432
|
+
* before the first pattern body. Trailing ``---`` separator visually
|
|
433
|
+
* distinguishes the preamble from the pattern markdown the agent is
|
|
434
|
+
* about to consume; an extra blank line above the separator keeps
|
|
435
|
+
* the markdown well-formed if the preamble already ends with one.
|
|
436
|
+
*/
|
|
437
|
+
function emitPoliciesPreamble(preamble) {
|
|
438
|
+
const trimmed = preamble.endsWith("\n") ? preamble : `${preamble}\n`;
|
|
439
|
+
process.stdout.write(trimmed);
|
|
440
|
+
process.stdout.write("\n---\n");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Fetch the workspace prose-rule policies for the current run from
|
|
445
|
+
* the Ship backend. The endpoint URL is derived from the callback
|
|
446
|
+
* URL by swapping the trailing ``/result`` segment for
|
|
447
|
+
* ``/policies-preamble`` — both share the same auth dependency
|
|
448
|
+
* (per-run JWT or long-lived ``SHIP_RUN_TOKEN``), so we can reuse
|
|
449
|
+
* the same bearer.
|
|
450
|
+
*
|
|
451
|
+
* Returns the preamble markdown or ``null`` when:
|
|
452
|
+
* - there's no callback URL / token (local invocation),
|
|
453
|
+
* - the URL doesn't end in ``/result`` (someone overrode the
|
|
454
|
+
* callback endpoint to a non-canonical path — too risky to
|
|
455
|
+
* guess),
|
|
456
|
+
* - the backend has no enabled policies (``preamble: null``),
|
|
457
|
+
* - or the request fails for any reason.
|
|
458
|
+
*
|
|
459
|
+
* Failures are surfaced as ``warn:`` lines on stderr so an operator
|
|
460
|
+
* can debug them without breaking the lane execution.
|
|
461
|
+
*/
|
|
462
|
+
async function fetchPoliciesPreamble(args) {
|
|
463
|
+
const callbackUrl = args.callbackUrl || process.env.SHIP_CALLBACK_URL;
|
|
464
|
+
if (!callbackUrl) return null;
|
|
465
|
+
const token = args.runToken || process.env.SHIP_RUN_TOKEN;
|
|
466
|
+
if (!token) return null;
|
|
467
|
+
if (!callbackUrl.endsWith("/result")) {
|
|
468
|
+
console.error(
|
|
469
|
+
`warn: SHIP_CALLBACK_URL does not end in /result; skipping policies-preamble fetch (got ${callbackUrl}).`,
|
|
470
|
+
);
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
const url = `${callbackUrl.slice(0, -"/result".length)}/policies-preamble`;
|
|
474
|
+
try {
|
|
475
|
+
const res = await fetch(url, {
|
|
476
|
+
method: "GET",
|
|
477
|
+
headers: {
|
|
478
|
+
Accept: "application/json",
|
|
479
|
+
Authorization: `Bearer ${token}`,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
if (!res.ok) {
|
|
483
|
+
console.error(
|
|
484
|
+
`warn: policies-preamble fetch returned HTTP ${res.status} ${res.statusText}; continuing without policies.`,
|
|
485
|
+
);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
const body = await res.json().catch(() => null);
|
|
489
|
+
if (!body || typeof body !== "object") return null;
|
|
490
|
+
const preamble = body.preamble;
|
|
491
|
+
if (typeof preamble !== "string" || !preamble.trim()) return null;
|
|
492
|
+
return preamble;
|
|
493
|
+
} catch (err) {
|
|
494
|
+
console.error(
|
|
495
|
+
`warn: policies-preamble fetch failed: ${err instanceof Error ? err.message : err}`,
|
|
496
|
+
);
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
282
499
|
}
|
|
283
500
|
|
|
501
|
+
|
|
284
502
|
/* ------------------------------------------------------------------ */
|
|
285
503
|
/* Helpers */
|
|
286
504
|
/* ------------------------------------------------------------------ */
|
|
@@ -288,6 +506,8 @@ export async function runCommand(ctx, rest) {
|
|
|
288
506
|
function parseArgs(rest) {
|
|
289
507
|
const out = {
|
|
290
508
|
lane: null,
|
|
509
|
+
pattern: null,
|
|
510
|
+
fanout: null,
|
|
291
511
|
trigger: null,
|
|
292
512
|
dryRun: false,
|
|
293
513
|
offline: false,
|
|
@@ -336,6 +556,8 @@ function parseArgs(rest) {
|
|
|
336
556
|
continue;
|
|
337
557
|
}
|
|
338
558
|
if (str("--lane", "lane")) continue;
|
|
559
|
+
if (str("--pattern", "pattern")) continue;
|
|
560
|
+
if (str("--fanout", "fanout")) continue;
|
|
339
561
|
if (str("--trigger", "trigger")) continue;
|
|
340
562
|
if (str("--ship-run-id", "runId")) continue;
|
|
341
563
|
if (str("--ship-callback-url", "callbackUrl")) continue;
|
|
@@ -352,6 +574,15 @@ function parseArgs(rest) {
|
|
|
352
574
|
`--trigger must be one of ${[...VALID_TRIGGERS].join("|")}; got ${out.trigger}`,
|
|
353
575
|
);
|
|
354
576
|
}
|
|
577
|
+
if (out.fanout && !LANE_FANOUT_MODES.includes(out.fanout)) {
|
|
578
|
+
die(
|
|
579
|
+
EXIT_USAGE,
|
|
580
|
+
`--fanout must be one of ${LANE_FANOUT_MODES.join("|")}; got ${out.fanout}`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (out.pattern !== null && (typeof out.pattern !== "string" || !out.pattern.trim())) {
|
|
584
|
+
die(EXIT_USAGE, "--pattern: must be a non-empty pattern id");
|
|
585
|
+
}
|
|
355
586
|
return out;
|
|
356
587
|
}
|
|
357
588
|
|
|
@@ -522,6 +753,15 @@ function fetchFromLockfile({ patternId, root, strict }) {
|
|
|
522
753
|
|
|
523
754
|
function resolveMethodologyBase(ctx, config) {
|
|
524
755
|
const fromFlag = ctx.baseUrl;
|
|
756
|
+
const fromEnv =
|
|
757
|
+
typeof process.env.SHIP_API_BASE === "string" && process.env.SHIP_API_BASE.trim()
|
|
758
|
+
? process.env.SHIP_API_BASE.trim().replace(/\/$/, "")
|
|
759
|
+
: null;
|
|
760
|
+
/* Wizard-seeded Actions secret: exact Ship API origin (``POST /fetch`` lives
|
|
761
|
+
* at the root next to ``/v1``). Do not append ``/api/methodology`` here. */
|
|
762
|
+
if (fromEnv) {
|
|
763
|
+
return fromEnv;
|
|
764
|
+
}
|
|
525
765
|
const raw = config?.api?.base_url;
|
|
526
766
|
if (typeof raw === "string" && raw.trim()) {
|
|
527
767
|
const u = raw.replace(/\/$/, "");
|
package/lib/commands/search.mjs
CHANGED
|
@@ -8,9 +8,9 @@ import { apiPost } from "../http.mjs";
|
|
|
8
8
|
export async function searchCommand(ctx, args) {
|
|
9
9
|
if (!args.length || args[0] === "help" || args[0] === "-h" || args[0] === "--help") {
|
|
10
10
|
console.log(`Usage:
|
|
11
|
-
|
|
11
|
+
shipctl search <query> [--top-k 8]
|
|
12
12
|
|
|
13
|
-
POST /search on the methodology API (same SHIP_API_BASE as
|
|
13
|
+
POST /search on the methodology API (same SHIP_API_BASE as shipctl pattern/tool/…).
|
|
14
14
|
|
|
15
15
|
Global flags: --base-url URL --json`);
|
|
16
16
|
return;
|