@elmundi/ship-cli 0.14.2 → 0.15.3
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 +17 -16
- package/bin/shipctl.mjs +4 -80
- package/lib/commands/feedback.mjs +1 -1
- package/lib/commands/help.mjs +47 -131
- package/lib/commands/init.mjs +17 -250
- package/lib/commands/knowledge.mjs +25 -328
- package/lib/commands/preflight.mjs +213 -0
- package/lib/commands/run.mjs +266 -116
- package/lib/commands/trigger.mjs +95 -10
- package/lib/config/schema.mjs +68 -11
- package/lib/http.mjs +0 -2
- package/lib/runtime/routines.mjs +34 -0
- package/lib/templates.mjs +2 -2
- package/lib/verify/checks/agents-on-disk.mjs +5 -28
- package/lib/verify/registry.mjs +7 -8
- package/package.json +1 -1
- package/lib/artifacts/fs-index.mjs +0 -230
- package/lib/cache/store.mjs +0 -422
- package/lib/commands/bootstrap.mjs +0 -4
- package/lib/commands/callback.mjs +0 -742
- package/lib/commands/docs.mjs +0 -90
- package/lib/commands/kickoff.mjs +0 -192
- package/lib/commands/lanes.mjs +0 -566
- package/lib/commands/manifest-catalog.mjs +0 -251
- package/lib/commands/migrate.mjs +0 -204
- package/lib/commands/new.mjs +0 -452
- package/lib/commands/patterns.mjs +0 -160
- package/lib/commands/process.mjs +0 -388
- package/lib/commands/search.mjs +0 -43
- package/lib/commands/sync.mjs +0 -824
- package/lib/config/migrate.mjs +0 -223
- package/lib/find-ship-root.mjs +0 -75
- package/lib/process/specialist-prompt-contract.mjs +0 -171
- package/lib/state/lockfile.mjs +0 -180
- package/lib/vendor/run-agent.workflow.yml +0 -254
- package/lib/verify/checks/artifacts-up-to-date.mjs +0 -78
- package/lib/verify/checks/cache-integrity.mjs +0 -51
- package/lib/verify/checks/gitignore-cache.mjs +0 -51
- package/lib/verify/checks/rules-markers.mjs +0 -135
package/lib/commands/run.mjs
CHANGED
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
* Customer's GH Actions cron fires this once per routine slot. The
|
|
5
5
|
* pipeline:
|
|
6
6
|
*
|
|
7
|
-
* 1. Read the routine from `.ship/config.yml` (
|
|
8
|
-
*
|
|
9
|
-
* 2.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
7
|
+
* 1. Read the routine from `.ship/config.yml` (specialist slug +
|
|
8
|
+
* optional inline prompt).
|
|
9
|
+
* 2. Resolve the agent role body via the workspace endpoint
|
|
10
|
+
* `GET /v1/workspaces/{ws}/agent-roles/{slug}/resolve` —
|
|
11
|
+
* workspace overrides win, otherwise the Ship default. Pull
|
|
12
|
+
* the `system` (shared base) body in parallel.
|
|
13
|
+
* 3. If the resolved role declares `fsm_stage`, ask Ship server
|
|
14
|
+
* for the next ticket in that stage
|
|
13
15
|
* (`GET /v1/.../tracker/next?state=<stage>`). Server picks the
|
|
14
16
|
* adapter (Linear / GH Issues / etc.) — CLI doesn't care.
|
|
15
|
-
* 4. Mint a `run_id` and render the prompt:
|
|
16
|
-
* details + a finish-protocol
|
|
17
|
-
*
|
|
18
|
-
* `FSM_STAGE` already substituted so the
|
|
19
|
-
* finish endpoint directly.
|
|
17
|
+
* 4. Mint a `run_id` and render the prompt: system body + role
|
|
18
|
+
* body + routine prompt + ticket details + a finish-protocol
|
|
19
|
+
* block with `SHIP_API_BASE`, `SHIP_API_TOKEN`, `SHIP_WORKSPACE_ID`,
|
|
20
|
+
* `RUN_ID`, `TICKET_REF`, `FSM_STAGE` already substituted so the
|
|
21
|
+
* agent can call the finish endpoint directly.
|
|
20
22
|
* 5. Launch the configured agent runtime (`cli/lib/agents/`) — Cursor
|
|
21
23
|
* Cloud today. Block until the runtime terminates.
|
|
22
24
|
* 6. The agent itself calls
|
|
@@ -46,15 +48,12 @@
|
|
|
46
48
|
import crypto from "node:crypto";
|
|
47
49
|
import path from "node:path";
|
|
48
50
|
|
|
49
|
-
import yaml from "yaml";
|
|
50
|
-
|
|
51
51
|
import { readConfig, findShipRoot } from "../config/io.mjs";
|
|
52
52
|
import { resolveExecutable } from "../runtime/routines.mjs";
|
|
53
|
-
import { fetchArtifact } from "../http.mjs";
|
|
54
|
-
import { readArtifactFile } from "../artifacts/fs-index.mjs";
|
|
55
|
-
import { resolveShipRepoRootForCatalog } from "../find-ship-root.mjs";
|
|
56
53
|
import { resolveProvider, runAgent } from "../agents/index.mjs";
|
|
57
54
|
|
|
55
|
+
const SYSTEM_ROLE_SLUG = "system";
|
|
56
|
+
|
|
58
57
|
|
|
59
58
|
const EXIT_OK = 0;
|
|
60
59
|
const EXIT_USAGE = 1;
|
|
@@ -73,8 +72,14 @@ export async function runCommand(ctx, rest) {
|
|
|
73
72
|
printHelp();
|
|
74
73
|
process.exit(EXIT_OK);
|
|
75
74
|
}
|
|
76
|
-
if (!args.routine) {
|
|
77
|
-
die(
|
|
75
|
+
if (!args.routine && !args.specialist) {
|
|
76
|
+
die(
|
|
77
|
+
EXIT_USAGE,
|
|
78
|
+
"either `--routine <id>` or `--specialist <slug>` is required.\nRun: shipctl run --help",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (args.routine && args.specialist) {
|
|
82
|
+
die(EXIT_USAGE, "`--routine` and `--specialist` are mutually exclusive.");
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
const cwd = args.cwd || process.cwd();
|
|
@@ -84,33 +89,80 @@ export async function runCommand(ctx, rest) {
|
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
const { config } = readConfig(cwd);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
// Routine mode: resolve from ``.ship/config.yml``. Specialist mode
|
|
93
|
+
// (used by the pipeline-pick fallback in the trigger workflow):
|
|
94
|
+
// synthesize a minimal executable so the rest of the pipeline can
|
|
95
|
+
// stay routine-shaped without inventing a ``pipeline:<slug>``
|
|
96
|
+
// routine in the YAML.
|
|
97
|
+
let resolved;
|
|
98
|
+
if (args.specialist) {
|
|
99
|
+
resolved = {
|
|
100
|
+
kind: "specialist",
|
|
101
|
+
id: args.specialist,
|
|
102
|
+
source: { specialist: args.specialist },
|
|
103
|
+
executable: {
|
|
104
|
+
id: args.specialist,
|
|
105
|
+
type: "specialist",
|
|
106
|
+
kind: "pipeline_pick",
|
|
107
|
+
specialist: args.specialist,
|
|
108
|
+
prompt: null,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
resolved = resolveExecutable(config, args.routine);
|
|
113
|
+
if (!resolved) {
|
|
114
|
+
die(EXIT_USAGE, `unknown routine '${args.routine}' in .ship/config.yml`);
|
|
115
|
+
}
|
|
90
116
|
}
|
|
91
117
|
|
|
118
|
+
// ``runId`` for emit/branch naming carries either the routine id or
|
|
119
|
+
// the specialist slug. Logging downstream uses ``runHandle``.
|
|
120
|
+
const runHandle = args.routine || `pipeline:${args.specialist}`;
|
|
121
|
+
|
|
92
122
|
const env = readEnv();
|
|
93
123
|
const { apiBase, apiToken, workspaceId, githubRepo } = env;
|
|
94
124
|
|
|
95
|
-
// 1) Resolve
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
// 1) Resolve specialist slug.
|
|
126
|
+
// ``routine.specialist`` is the canonical Phase-2.4 form; the legacy
|
|
127
|
+
// ``routine.pattern`` is mapped to a slug in
|
|
128
|
+
// ``cli/lib/runtime/routines.mjs`` (drops the ``role-`` prefix).
|
|
129
|
+
const specialistSlug = resolved.executable.specialist || args.specialist;
|
|
130
|
+
if (!specialistSlug) {
|
|
131
|
+
die(
|
|
132
|
+
EXIT_USAGE,
|
|
133
|
+
`routine '${args.routine}' has no 'specialist:' (or legacy 'pattern:') set`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (!apiBase || !apiToken || !workspaceId) {
|
|
137
|
+
die(
|
|
138
|
+
EXIT_USAGE,
|
|
139
|
+
"agent-role resolve requires SHIP_API_BASE + SHIP_API_TOKEN + SHIP_WORKSPACE_ID",
|
|
140
|
+
);
|
|
99
141
|
}
|
|
100
142
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
// 2) Pull the resolved role + the shared system prompt in parallel.
|
|
144
|
+
// ``resolveAgentRole`` returns the workspace override when present,
|
|
145
|
+
// otherwise falls back to the Ship default. ``system`` is fetched
|
|
146
|
+
// optional — older deployments may not have it; we render without
|
|
147
|
+
// a system header in that case.
|
|
148
|
+
const [roleResolved, systemResolved] = await Promise.all([
|
|
149
|
+
resolveAgentRole({ apiBase, apiToken, workspaceId, slug: specialistSlug }),
|
|
150
|
+
resolveAgentRole({
|
|
151
|
+
apiBase,
|
|
152
|
+
apiToken,
|
|
153
|
+
workspaceId,
|
|
154
|
+
slug: SYSTEM_ROLE_SLUG,
|
|
155
|
+
optional: true,
|
|
156
|
+
}),
|
|
157
|
+
]);
|
|
158
|
+
if (!roleResolved) {
|
|
159
|
+
die(EXIT_USAGE, `unknown agent role '${specialistSlug}' for this workspace`);
|
|
160
|
+
}
|
|
161
|
+
const fsmStage = roleResolved.fsm_stage || null;
|
|
162
|
+
const roleBody = roleResolved.prompt || "";
|
|
163
|
+
const systemBody = systemResolved?.prompt || "";
|
|
112
164
|
|
|
113
|
-
//
|
|
165
|
+
// 3) Resolve task. ``--dry-run`` skips the server call and uses a
|
|
114
166
|
// synthetic task so the operator can see the prompt shape without
|
|
115
167
|
// needing the new endpoints deployed.
|
|
116
168
|
let task = null;
|
|
@@ -134,20 +186,40 @@ export async function runCommand(ctx, rest) {
|
|
|
134
186
|
state: fsmStage,
|
|
135
187
|
});
|
|
136
188
|
if (!task) {
|
|
137
|
-
emit(args, {
|
|
189
|
+
emit(args, {
|
|
190
|
+
status: "noop",
|
|
191
|
+
routine: args.routine,
|
|
192
|
+
specialist: specialistSlug,
|
|
193
|
+
fsm_stage: fsmStage,
|
|
194
|
+
reason: "no_eligible_ticket",
|
|
195
|
+
run_handle: runHandle,
|
|
196
|
+
});
|
|
138
197
|
process.exit(EXIT_NO_TASK);
|
|
139
198
|
}
|
|
140
199
|
}
|
|
141
200
|
}
|
|
142
201
|
|
|
143
|
-
//
|
|
202
|
+
// 4) Fetch the workspace policy preamble so the agent prompt
|
|
203
|
+
// carries the same standing rules the Navigator chat does. Best-
|
|
204
|
+
// effort: a missing token, missing API base, or a network failure
|
|
205
|
+
// quietly skips the prepend — local / offline runs still work.
|
|
206
|
+
// ``role`` is now the specialist slug directly (no more
|
|
207
|
+
// ``spec.role`` indirection).
|
|
208
|
+
const policiesPreamble = await fetchPoliciesPreamble({
|
|
209
|
+
apiBase,
|
|
210
|
+
apiToken,
|
|
211
|
+
workspaceId,
|
|
212
|
+
role: specialistSlug,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// 5) Mint a run_id + render prompt with finish-protocol values
|
|
144
216
|
// already substituted so the agent can call /agent-runs/finish from
|
|
145
217
|
// inside Cursor without holding any extra config.
|
|
146
218
|
const runId = `run_${crypto.randomBytes(8).toString("hex")}`;
|
|
147
|
-
const
|
|
148
|
-
patternBody,
|
|
149
|
-
baseBody,
|
|
150
|
-
role:
|
|
219
|
+
const renderedPrompt = renderPrompt({
|
|
220
|
+
patternBody: roleBody,
|
|
221
|
+
baseBody: systemBody,
|
|
222
|
+
role: specialistSlug,
|
|
151
223
|
routineSpec: resolved.executable,
|
|
152
224
|
task,
|
|
153
225
|
fsmStage,
|
|
@@ -156,27 +228,31 @@ export async function runCommand(ctx, rest) {
|
|
|
156
228
|
apiToken,
|
|
157
229
|
workspaceId,
|
|
158
230
|
runId,
|
|
159
|
-
role:
|
|
231
|
+
role: specialistSlug,
|
|
160
232
|
ticketRef: task?.ticket_ref || null,
|
|
161
233
|
fsmStage: fsmStage || null,
|
|
162
234
|
},
|
|
163
235
|
});
|
|
236
|
+
const prompt = policiesPreamble
|
|
237
|
+
? `${policiesPreamble.trim()}\n\n---\n\n${renderedPrompt}`
|
|
238
|
+
: renderedPrompt;
|
|
164
239
|
|
|
165
240
|
// ``--dry-run`` exits here so the operator can eyeball the rendered
|
|
166
241
|
// prompt + resolved task without launching an agent or touching any
|
|
167
|
-
// tracker. Useful when iterating on
|
|
242
|
+
// tracker. Useful when iterating on prompts.
|
|
168
243
|
if (args.dryRun || ctx.dryRun) {
|
|
169
244
|
if (args.json) {
|
|
170
245
|
console.log(JSON.stringify({
|
|
171
246
|
status: "dry-run",
|
|
172
247
|
routine: args.routine,
|
|
173
|
-
|
|
248
|
+
specialist: specialistSlug,
|
|
249
|
+
specialist_source: roleResolved.source,
|
|
174
250
|
fsm_stage: fsmStage,
|
|
175
251
|
task,
|
|
176
252
|
prompt,
|
|
177
253
|
}, null, 2));
|
|
178
254
|
} else {
|
|
179
|
-
console.error(`# ship: dry-run
|
|
255
|
+
console.error(`# ship: dry-run handle=${runHandle} specialist=${specialistSlug} (${roleResolved.source}) fsm_stage=${fsmStage || "(context-free)"}`);
|
|
180
256
|
if (task) {
|
|
181
257
|
console.error(`# ship: task ticket_ref=${task.ticket_ref} title=${JSON.stringify(task.title || "")}`);
|
|
182
258
|
} else {
|
|
@@ -189,8 +265,8 @@ export async function runCommand(ctx, rest) {
|
|
|
189
265
|
}
|
|
190
266
|
|
|
191
267
|
// 4) Launch agent runtime
|
|
192
|
-
const provider = resolveProvider(config, args.routine);
|
|
193
|
-
const branchName = makeBranchName(
|
|
268
|
+
const provider = resolveProvider(config, args.routine || specialistSlug);
|
|
269
|
+
const branchName = makeBranchName(runHandle, task?.ticket_ref);
|
|
194
270
|
const repoUrl = githubRepo ? `https://github.com/${githubRepo}` : null;
|
|
195
271
|
if (!repoUrl) die(EXIT_USAGE, "GITHUB_REPOSITORY env var is required to launch agent");
|
|
196
272
|
|
|
@@ -207,8 +283,9 @@ export async function runCommand(ctx, rest) {
|
|
|
207
283
|
emit(args, {
|
|
208
284
|
status: "error",
|
|
209
285
|
routine: args.routine,
|
|
210
|
-
|
|
286
|
+
specialist: specialistSlug,
|
|
211
287
|
run_id: runId,
|
|
288
|
+
run_handle: runHandle,
|
|
212
289
|
stage: "launch_agent",
|
|
213
290
|
error: err instanceof Error ? err.message : String(err),
|
|
214
291
|
});
|
|
@@ -223,13 +300,14 @@ export async function runCommand(ctx, rest) {
|
|
|
223
300
|
emit(args, {
|
|
224
301
|
status: "completed",
|
|
225
302
|
routine: args.routine,
|
|
226
|
-
|
|
303
|
+
specialist: specialistSlug,
|
|
227
304
|
fsm_stage: fsmStage,
|
|
228
305
|
ticket_ref: task?.ticket_ref || null,
|
|
229
306
|
agent_id: runtime.agentId,
|
|
230
307
|
branch: runtime.branchName,
|
|
231
308
|
cursor_status: runtime.status,
|
|
232
309
|
run_id: runId,
|
|
310
|
+
run_handle: runHandle,
|
|
233
311
|
});
|
|
234
312
|
process.exit(EXIT_OK);
|
|
235
313
|
}
|
|
@@ -256,49 +334,107 @@ function stripSlash(s) {
|
|
|
256
334
|
}
|
|
257
335
|
|
|
258
336
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Resolve an agent role through the workspace endpoint.
|
|
339
|
+
*
|
|
340
|
+
* Returns ``{slug, name, prompt, fsm_stage, source}`` (workspace row
|
|
341
|
+
* or Ship default), ``null`` for 404, throws on auth/network errors
|
|
342
|
+
* unless ``optional`` is set (then 404 *and* errors return ``null``
|
|
343
|
+
* with a one-line warning).
|
|
344
|
+
*/
|
|
345
|
+
async function resolveAgentRole({
|
|
346
|
+
apiBase,
|
|
347
|
+
apiToken,
|
|
348
|
+
workspaceId,
|
|
349
|
+
slug,
|
|
350
|
+
optional = false,
|
|
351
|
+
}) {
|
|
352
|
+
const url = `${apiBase}/v1/workspaces/${encodeURIComponent(workspaceId)}/agent-roles/${encodeURIComponent(slug)}/resolve`;
|
|
353
|
+
let res;
|
|
354
|
+
try {
|
|
355
|
+
res = await fetch(url, {
|
|
356
|
+
headers: {
|
|
357
|
+
Accept: "application/json",
|
|
358
|
+
Authorization: `Bearer ${apiToken}`,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
} catch (err) {
|
|
362
|
+
if (optional) {
|
|
363
|
+
console.error(
|
|
364
|
+
`warn: agent-role '${slug}' resolve failed (network): ${err instanceof Error ? err.message : err}`,
|
|
365
|
+
);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
throw err;
|
|
265
369
|
}
|
|
266
|
-
|
|
370
|
+
if (res.status === 404) return null;
|
|
371
|
+
if (!res.ok) {
|
|
372
|
+
if (optional) {
|
|
373
|
+
console.error(
|
|
374
|
+
`warn: agent-role '${slug}' resolve returned ${res.status}; running without it`,
|
|
375
|
+
);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
throw new Error(
|
|
379
|
+
`agent-roles resolve ${res.status}: ${(await res.text()).slice(0, 300)}`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return res.json();
|
|
267
383
|
}
|
|
268
384
|
|
|
269
385
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Best-effort fetch of the workspace's policy preamble. Returns the
|
|
388
|
+
* markdown block to prepend, or ``null`` when there's nothing to
|
|
389
|
+
* inject (no policies, missing token / API base, network error,
|
|
390
|
+
* non-200 response). Never throws — a broken policies path mustn't
|
|
391
|
+
* break a routine run.
|
|
392
|
+
*
|
|
393
|
+
* Auth: workspace-membership token (``SHIP_API_TOKEN`` — same one
|
|
394
|
+
* the rest of ``run.mjs`` uses). The companion run-token endpoint
|
|
395
|
+
* at ``/v1/pipelines/runs/{run_id}/policies-preamble`` doesn't fit
|
|
396
|
+
* this flow because the CLI mints ``run_id`` locally; the
|
|
397
|
+
* workspace-scoped variant takes membership instead.
|
|
398
|
+
*/
|
|
399
|
+
async function fetchPoliciesPreamble({ apiBase, apiToken, workspaceId, role }) {
|
|
400
|
+
if (!apiBase || !apiToken || !workspaceId) return null;
|
|
401
|
+
const qs = role ? `?role=${encodeURIComponent(role)}` : "";
|
|
402
|
+
const url = `${apiBase}/v1/workspaces/${encodeURIComponent(workspaceId)}/policies/preamble${qs}`;
|
|
403
|
+
let res;
|
|
277
404
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
405
|
+
res = await fetch(url, {
|
|
406
|
+
headers: {
|
|
407
|
+
Accept: "application/json",
|
|
408
|
+
Authorization: `Bearer ${apiToken}`,
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
} catch (err) {
|
|
412
|
+
console.error(
|
|
413
|
+
`warn: policies preamble fetch failed (network): ${err instanceof Error ? err.message : err}`,
|
|
414
|
+
);
|
|
415
|
+
return null;
|
|
285
416
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// or without quotes) anywhere in the spec block.
|
|
298
|
-
const m = frontmatterRaw.match(/^\s*fsm_stage:\s*['"]?([\w.-]+)['"]?/m);
|
|
299
|
-
if (m && m[1]) return m[1].trim();
|
|
417
|
+
if (!res.ok) {
|
|
418
|
+
// 404 happens against older backends that don't have the
|
|
419
|
+
// workspace-scoped endpoint yet — silent skip there. Other
|
|
420
|
+
// statuses (401, 403, 500) get a one-line warning so operators
|
|
421
|
+
// notice misconfigurations without aborting the run.
|
|
422
|
+
if (res.status !== 404) {
|
|
423
|
+
console.error(
|
|
424
|
+
`warn: policies preamble fetch returned ${res.status}; running without preamble`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
300
428
|
}
|
|
301
|
-
|
|
429
|
+
let body;
|
|
430
|
+
try {
|
|
431
|
+
body = await res.json();
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
return typeof body?.preamble === "string" && body.preamble.trim()
|
|
436
|
+
? body.preamble
|
|
437
|
+
: null;
|
|
302
438
|
}
|
|
303
439
|
|
|
304
440
|
|
|
@@ -345,6 +481,8 @@ function renderPrompt({ patternBody, baseBody, role, routineSpec, task, fsmStage
|
|
|
345
481
|
out.push("");
|
|
346
482
|
}
|
|
347
483
|
out.push(expanded.trim());
|
|
484
|
+
out.push("");
|
|
485
|
+
out.push(renderLifecycleHooks());
|
|
348
486
|
if (task) {
|
|
349
487
|
out.push("");
|
|
350
488
|
out.push("## Task");
|
|
@@ -367,6 +505,33 @@ function renderPrompt({ patternBody, baseBody, role, routineSpec, task, fsmStage
|
|
|
367
505
|
}
|
|
368
506
|
|
|
369
507
|
|
|
508
|
+
function renderLifecycleHooks() {
|
|
509
|
+
// Phase 4: every run is bracketed by two auditable lifecycle hooks
|
|
510
|
+
// — knowledge-fetch first, knowledge-feedback last. We render them
|
|
511
|
+
// as explicit prompt instructions so they show up in the agent's
|
|
512
|
+
// tool-use log (the runner audits the log to flag runs that
|
|
513
|
+
// skipped them). Soft for now (no run-blocking enforcement) but
|
|
514
|
+
// the prompt is the canonical contract until the runner grows
|
|
515
|
+
// tool-stream interception.
|
|
516
|
+
return [
|
|
517
|
+
"## Lifecycle hooks (Phase 4)",
|
|
518
|
+
"",
|
|
519
|
+
"**First call — knowledge fetch.** Before any other tool call,",
|
|
520
|
+
`run \`shipctl knowledge fetch <bucket>\` for at least one bucket`,
|
|
521
|
+
"relevant to your role. The audit log flags runs whose first",
|
|
522
|
+
"tool call wasn't a knowledge fetch; do not skip this to save",
|
|
523
|
+
"tokens.",
|
|
524
|
+
"",
|
|
525
|
+
"**Last call — knowledge feedback.** Before calling the finish",
|
|
526
|
+
"endpoint, leave one-line learnings via the Ship knowledge",
|
|
527
|
+
"feedback channel (`shipctl feedback draft` → `feedback submit`)",
|
|
528
|
+
"if you discovered something a future run on this codebase would",
|
|
529
|
+
"want to know. Empty findings are a valid outcome — better no",
|
|
530
|
+
"feedback than fabricated polish.",
|
|
531
|
+
].join("\n");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
|
|
370
535
|
function renderExitProtocol(ctx) {
|
|
371
536
|
// Substitute the run-time values directly into the example so the
|
|
372
537
|
// agent doesn't have to figure out env var hookup. The token is
|
|
@@ -470,33 +635,6 @@ as a one-shot credential for this run.
|
|
|
470
635
|
}
|
|
471
636
|
|
|
472
637
|
|
|
473
|
-
/**
|
|
474
|
-
* Load a pattern body. Resolution order:
|
|
475
|
-
* 1) when running inside the Ship monorepo, read from
|
|
476
|
-
* ``artifacts/patterns/<id>/ARTIFACT.md`` on disk — fast and
|
|
477
|
-
* always reflects the working tree (good for dry-runs / local
|
|
478
|
-
* smoke tests before the server is rebuilt).
|
|
479
|
-
* 2) otherwise hit the server's ``POST /fetch``.
|
|
480
|
-
*/
|
|
481
|
-
async function loadPattern({ id, fetchBase, optional = false }) {
|
|
482
|
-
const shipRepo = resolveShipRepoRootForCatalog();
|
|
483
|
-
if (shipRepo) {
|
|
484
|
-
const file = readArtifactFile(shipRepo, "pattern", id);
|
|
485
|
-
if (file && typeof file.content === "string") return file.content;
|
|
486
|
-
}
|
|
487
|
-
try {
|
|
488
|
-
const { content } = await fetchArtifact(fetchBase, "pattern", id);
|
|
489
|
-
return content;
|
|
490
|
-
} catch (err) {
|
|
491
|
-
if (optional) {
|
|
492
|
-
console.error(`warn: failed to fetch pattern '${id}': ${err.message}`);
|
|
493
|
-
return "";
|
|
494
|
-
}
|
|
495
|
-
throw err;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
638
|
function makeBranchName(routine, ticketRef) {
|
|
501
639
|
const stamp = Date.now().toString(36);
|
|
502
640
|
if (ticketRef) {
|
|
@@ -513,7 +651,14 @@ function makeBranchName(routine, ticketRef) {
|
|
|
513
651
|
|
|
514
652
|
|
|
515
653
|
function parseArgs(rest) {
|
|
516
|
-
const out = {
|
|
654
|
+
const out = {
|
|
655
|
+
routine: null,
|
|
656
|
+
specialist: null,
|
|
657
|
+
cwd: null,
|
|
658
|
+
json: false,
|
|
659
|
+
help: false,
|
|
660
|
+
dryRun: false,
|
|
661
|
+
};
|
|
517
662
|
const copy = [...rest];
|
|
518
663
|
while (copy.length) {
|
|
519
664
|
const a = copy[0];
|
|
@@ -521,6 +666,7 @@ function parseArgs(rest) {
|
|
|
521
666
|
if (a === "--json") { out.json = true; copy.shift(); continue; }
|
|
522
667
|
if (a === "--dry-run") { out.dryRun = true; copy.shift(); continue; }
|
|
523
668
|
if (a === "--routine" && copy[1] !== undefined) { out.routine = copy[1]; copy.splice(0, 2); continue; }
|
|
669
|
+
if (a === "--specialist" && copy[1] !== undefined) { out.specialist = copy[1]; copy.splice(0, 2); continue; }
|
|
524
670
|
if (a === "--cwd" && copy[1] !== undefined) { out.cwd = path.resolve(copy[1]); copy.splice(0, 2); continue; }
|
|
525
671
|
// Soft-ignore legacy flags that older trigger workflows still pass —
|
|
526
672
|
// the new pipeline doesn't need them and refusing would break repos
|
|
@@ -535,10 +681,14 @@ function parseArgs(rest) {
|
|
|
535
681
|
|
|
536
682
|
|
|
537
683
|
function printHelp() {
|
|
538
|
-
console.log(`shipctl run — execute one E14 routine end-to-end.
|
|
684
|
+
console.log(`shipctl run — execute one E14 routine or pipeline-pick specialist end-to-end.
|
|
539
685
|
|
|
540
686
|
Run
|
|
541
687
|
shipctl run --routine <id> [--json] [--cwd <dir>] [--dry-run]
|
|
688
|
+
shipctl run --specialist <slug> [--json] [--cwd <dir>] [--dry-run]
|
|
689
|
+
|
|
690
|
+
--routine id from process.routines in .ship/config.yml (cron-driven)
|
|
691
|
+
--specialist agent-role slug from the Ship registry (pipeline-pick fallback)
|
|
542
692
|
|
|
543
693
|
ENV
|
|
544
694
|
SHIP_API_BASE Ship server base URL (e.g. https://api.ship.elmundi.com)
|