@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.
Files changed (39) hide show
  1. package/README.md +17 -16
  2. package/bin/shipctl.mjs +4 -80
  3. package/lib/commands/feedback.mjs +1 -1
  4. package/lib/commands/help.mjs +47 -131
  5. package/lib/commands/init.mjs +17 -250
  6. package/lib/commands/knowledge.mjs +25 -328
  7. package/lib/commands/preflight.mjs +213 -0
  8. package/lib/commands/run.mjs +266 -116
  9. package/lib/commands/trigger.mjs +95 -10
  10. package/lib/config/schema.mjs +68 -11
  11. package/lib/http.mjs +0 -2
  12. package/lib/runtime/routines.mjs +34 -0
  13. package/lib/templates.mjs +2 -2
  14. package/lib/verify/checks/agents-on-disk.mjs +5 -28
  15. package/lib/verify/registry.mjs +7 -8
  16. package/package.json +1 -1
  17. package/lib/artifacts/fs-index.mjs +0 -230
  18. package/lib/cache/store.mjs +0 -422
  19. package/lib/commands/bootstrap.mjs +0 -4
  20. package/lib/commands/callback.mjs +0 -742
  21. package/lib/commands/docs.mjs +0 -90
  22. package/lib/commands/kickoff.mjs +0 -192
  23. package/lib/commands/lanes.mjs +0 -566
  24. package/lib/commands/manifest-catalog.mjs +0 -251
  25. package/lib/commands/migrate.mjs +0 -204
  26. package/lib/commands/new.mjs +0 -452
  27. package/lib/commands/patterns.mjs +0 -160
  28. package/lib/commands/process.mjs +0 -388
  29. package/lib/commands/search.mjs +0 -43
  30. package/lib/commands/sync.mjs +0 -824
  31. package/lib/config/migrate.mjs +0 -223
  32. package/lib/find-ship-root.mjs +0 -75
  33. package/lib/process/specialist-prompt-contract.mjs +0 -171
  34. package/lib/state/lockfile.mjs +0 -180
  35. package/lib/vendor/run-agent.workflow.yml +0 -254
  36. package/lib/verify/checks/artifacts-up-to-date.mjs +0 -78
  37. package/lib/verify/checks/cache-integrity.mjs +0 -51
  38. package/lib/verify/checks/gitignore-cache.mjs +0 -51
  39. package/lib/verify/checks/rules-markers.mjs +0 -135
@@ -1,388 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- import { readConfig } from "../config/io.mjs";
5
- import { CONFIG_SCHEMA_VERSION, validateConfig } from "../config/schema.mjs";
6
- import { apiGet } from "../http.mjs";
7
- import {
8
- buildSpecialistPromptBundle,
9
- renderSpecialistPromptBundleMarkdown,
10
- } from "../process/specialist-prompt-contract.mjs";
11
-
12
- export async function processCommand(ctx, rest) {
13
- const [sub, ...args] = rest;
14
- if (!sub || sub === "help" || sub === "-h" || sub === "--help") {
15
- printProcessHelp();
16
- return;
17
- }
18
- if (sub === "prompt" || sub === "bundle") {
19
- await processPromptCommand(ctx, args);
20
- return;
21
- }
22
- if (sub === "tickets") {
23
- await processTicketsCommand(ctx, args);
24
- return;
25
- }
26
- console.error(
27
- `Unknown 'shipctl process' subcommand: ${sub}\nRun: shipctl process --help`,
28
- );
29
- process.exit(1);
30
- }
31
-
32
- function printProcessHelp() {
33
- console.log(`shipctl process — assemble Process/FSM specialist context
34
-
35
- USAGE
36
- shipctl process prompt --state <state-id> [--ticket-json <json>] [--ticket-file <path>]
37
- [--ticket-id <id>] [--ticket-title <title>] [--ticket-url <url>]
38
- [--ticket-description <text>] [--policies-file <path>]
39
- [--cwd <dir>] [--json]
40
- shipctl process prompt --specialist <specialist-id> [...]
41
- shipctl process tickets --workspace <id> [--process <id>] [--tracker <kind>]
42
- [--query <text>] [--state open|closed|all] [--limit <n>]
43
- [--project-hint <hint>] [--assignee-me] [--assignee <user>]
44
- [--json]
45
-
46
- WHAT IT EMITS
47
- A specialist prompt bundle assembled from .ship/config.yml:
48
- - process and state identity
49
- - specialist template fields from the selected state
50
- - ticket context supplied by the caller / tracker picker
51
- - allowed outgoing FSM transitions from process.transitions
52
- - workspace policies supplied by --policies-file
53
- - mandatory knowledge-first and Ship-boundary guardrails
54
-
55
- The command does not mutate tracker tickets, execute transitions, or write to
56
- the repository. It prepares context for an agent runtime; Ship remains
57
- responsible for side effects, FSM validation, audit logging, and PR-only writes.`);
58
- }
59
-
60
- async function processTicketsCommand(ctx, args) {
61
- const opts = parseTicketsArgs(args);
62
- const baseUrl = resolveWorkspaceBaseUrl(opts.baseUrl || ctx.baseUrl);
63
- const token = process.env.SHIP_API_TOKEN || "";
64
- if (!token) {
65
- console.error("SHIP_API_TOKEN is required for shipctl process tickets.");
66
- process.exit(1);
67
- }
68
- const processId = opts.processId || "development";
69
- const params = new URLSearchParams();
70
- for (const [key, value] of Object.entries({
71
- tracker: opts.tracker,
72
- project_hint: opts.projectHint,
73
- state: opts.state,
74
- query: opts.query,
75
- assignee: opts.assignee,
76
- limit: opts.limit,
77
- })) {
78
- if (value !== null && value !== undefined && value !== "") {
79
- params.set(key, String(value));
80
- }
81
- }
82
- if (opts.assigneeMe) params.set("assignee_me", "true");
83
- const suffix = params.toString() ? `?${params}` : "";
84
- const data = await apiGet(
85
- baseUrl,
86
- `/v1/workspaces/${encodeURIComponent(opts.workspace)}/processes/${encodeURIComponent(processId)}/tickets${suffix}`,
87
- );
88
- if (ctx.json || opts.json) {
89
- console.log(JSON.stringify(data, null, 2));
90
- return;
91
- }
92
- const tickets = Array.isArray(data?.tickets) ? data.tickets : [];
93
- console.log(`Tracker: ${data?.tracker || opts.tracker || "(auto)"}`);
94
- if (!tickets.length) {
95
- console.log("No tickets matched.");
96
- return;
97
- }
98
- for (const ticket of tickets) {
99
- const id = ticket.key || ticket.display_id || ticket.id || "(no id)";
100
- const title = ticket.title || "(untitled)";
101
- const status = ticket.status ? ` [${ticket.status}]` : "";
102
- const url = ticket.url ? `\n ${ticket.url}` : "";
103
- console.log(`- ${id}${status}: ${title}${url}`);
104
- }
105
- }
106
-
107
- async function processPromptCommand(ctx, args) {
108
- const opts = parsePromptArgs(args);
109
- const cwd = opts.cwd || process.cwd();
110
- let config;
111
- try {
112
- config = readConfig(cwd).config;
113
- } catch (err) {
114
- die(err instanceof Error ? err.message : String(err));
115
- }
116
- if (config.version !== CONFIG_SCHEMA_VERSION) {
117
- die(
118
- `.ship/config.yml is at v${config.version}; shipctl process prompt requires v${CONFIG_SCHEMA_VERSION}.\nRun 'shipctl migrate' to upgrade.`,
119
- );
120
- }
121
- const validation = validateConfig(config);
122
- if (!validation.ok) {
123
- die(["config is invalid:", ...validation.errors.map((e) => ` - ${e}`)].join("\n"));
124
- }
125
- const processConfig = config.process;
126
- if (!processConfig || typeof processConfig !== "object") {
127
- die("process: missing from .ship/config.yml");
128
- }
129
-
130
- const state = selectState(processConfig, opts);
131
- const allowedTransitions = Array.isArray(processConfig.transitions)
132
- ? processConfig.transitions.filter((transition) => transition.from === state.id)
133
- : [];
134
- const bundle = buildSpecialistPromptBundle({
135
- process: processConfig,
136
- state,
137
- allowedTransitions,
138
- ticket: resolveTicket(opts),
139
- policies: readOptionalFile(opts.policiesFile, "policies"),
140
- });
141
-
142
- if (ctx.json || opts.json) {
143
- console.log(JSON.stringify(bundle, null, 2));
144
- return;
145
- }
146
- process.stdout.write(renderSpecialistPromptBundleMarkdown(bundle));
147
- }
148
-
149
- function parsePromptArgs(args) {
150
- const out = {
151
- state: null,
152
- specialist: null,
153
- cwd: null,
154
- json: false,
155
- ticketJson: null,
156
- ticketFile: null,
157
- ticket: {},
158
- policiesFile: null,
159
- };
160
- const copy = [...args];
161
- const readValue = (flag) => {
162
- const current = copy.shift();
163
- if (current === flag) {
164
- if (copy.length === 0) die(`${flag} requires a value`);
165
- return String(copy.shift());
166
- }
167
- const prefix = `${flag}=`;
168
- if (current && current.startsWith(prefix)) {
169
- return current.slice(prefix.length);
170
- }
171
- copy.unshift(current);
172
- return null;
173
- };
174
-
175
- while (copy.length) {
176
- const arg = copy[0];
177
- if (arg === "--help" || arg === "-h") {
178
- printProcessHelp();
179
- process.exit(0);
180
- }
181
- if (arg === "--json") {
182
- copy.shift();
183
- out.json = true;
184
- continue;
185
- }
186
- const state = readValue("--state");
187
- if (state !== null) {
188
- out.state = state;
189
- continue;
190
- }
191
- const specialist = readValue("--specialist");
192
- if (specialist !== null) {
193
- out.specialist = specialist;
194
- continue;
195
- }
196
- const cwd = readValue("--cwd");
197
- if (cwd !== null) {
198
- out.cwd = path.resolve(cwd);
199
- continue;
200
- }
201
- const ticketJson = readValue("--ticket-json");
202
- if (ticketJson !== null) {
203
- out.ticketJson = ticketJson;
204
- continue;
205
- }
206
- const ticketFile = readValue("--ticket-file");
207
- if (ticketFile !== null) {
208
- out.ticketFile = path.resolve(ticketFile);
209
- continue;
210
- }
211
- const policiesFile = readValue("--policies-file");
212
- if (policiesFile !== null) {
213
- out.policiesFile = path.resolve(policiesFile);
214
- continue;
215
- }
216
- for (const [flag, key] of [
217
- ["--ticket-id", "id"],
218
- ["--ticket-key", "key"],
219
- ["--ticket-title", "title"],
220
- ["--ticket-url", "url"],
221
- ["--ticket-status", "status"],
222
- ["--ticket-description", "description"],
223
- ]) {
224
- const value = readValue(flag);
225
- if (value !== null) {
226
- out.ticket[key] = value;
227
- break;
228
- }
229
- }
230
- if (copy[0] === arg) {
231
- die(`unknown argument: ${arg}\nRun: shipctl process prompt --help`);
232
- }
233
- }
234
- if (!out.state && !out.specialist) {
235
- die("either --state <state-id> or --specialist <specialist-id> is required");
236
- }
237
- if (out.state && out.specialist) {
238
- die("use either --state or --specialist, not both");
239
- }
240
- return out;
241
- }
242
-
243
- function parseTicketsArgs(args) {
244
- const out = {
245
- workspace: null,
246
- processId: "development",
247
- tracker: null,
248
- projectHint: null,
249
- query: null,
250
- state: "open",
251
- limit: 10,
252
- assigneeMe: false,
253
- assignee: null,
254
- baseUrl: null,
255
- json: false,
256
- };
257
- const copy = [...args];
258
- const readValue = (flag) => {
259
- const current = copy.shift();
260
- if (current === flag) {
261
- if (copy.length === 0) die(`${flag} requires a value`);
262
- return String(copy.shift());
263
- }
264
- const prefix = `${flag}=`;
265
- if (current && current.startsWith(prefix)) {
266
- return current.slice(prefix.length);
267
- }
268
- copy.unshift(current);
269
- return null;
270
- };
271
- while (copy.length) {
272
- const arg = copy[0];
273
- if (arg === "--help" || arg === "-h") {
274
- printProcessHelp();
275
- process.exit(0);
276
- }
277
- if (arg === "--json") {
278
- copy.shift();
279
- out.json = true;
280
- continue;
281
- }
282
- if (arg === "--assignee-me") {
283
- copy.shift();
284
- out.assigneeMe = true;
285
- continue;
286
- }
287
- for (const [flag, key] of [
288
- ["--workspace", "workspace"],
289
- ["--process", "processId"],
290
- ["--tracker", "tracker"],
291
- ["--project-hint", "projectHint"],
292
- ["--query", "query"],
293
- ["--state", "state"],
294
- ["--limit", "limit"],
295
- ["--assignee", "assignee"],
296
- ["--base-url", "baseUrl"],
297
- ]) {
298
- const value = readValue(flag);
299
- if (value !== null) {
300
- out[key] = key === "limit" ? Number(value) : value;
301
- break;
302
- }
303
- }
304
- if (copy[0] === arg) {
305
- die(`unknown argument: ${arg}\nRun: shipctl process tickets --help`);
306
- }
307
- }
308
- if (!out.workspace) die("--workspace <id> is required");
309
- if (!["open", "closed", "all"].includes(out.state)) {
310
- die("--state must be one of open|closed|all");
311
- }
312
- if (!Number.isInteger(out.limit) || out.limit < 1 || out.limit > 25) {
313
- die("--limit must be an integer between 1 and 25");
314
- }
315
- return out;
316
- }
317
-
318
- function selectState(processConfig, opts) {
319
- const states = Array.isArray(processConfig.states) ? processConfig.states : [];
320
- if (opts.state) {
321
- const state = states.find((item) => item.id === opts.state);
322
- if (!state) die(`unknown process state ${JSON.stringify(opts.state)}`);
323
- return state;
324
- }
325
- const state = states.find((item) => {
326
- const specialist = item.specialist;
327
- if (typeof specialist === "string") return specialist === opts.specialist;
328
- if (specialist && typeof specialist === "object") {
329
- return specialist.id === opts.specialist || specialist.name === opts.specialist;
330
- }
331
- return false;
332
- });
333
- if (!state) die(`unknown process specialist ${JSON.stringify(opts.specialist)}`);
334
- return state;
335
- }
336
-
337
- function resolveTicket(opts) {
338
- const parts = [];
339
- if (opts.ticketFile) {
340
- const body = readOptionalFile(opts.ticketFile, "ticket");
341
- if (body) {
342
- parts.push(parseTicketJson(body, `ticket file ${opts.ticketFile}`));
343
- }
344
- }
345
- if (opts.ticketJson) {
346
- parts.push(parseTicketJson(opts.ticketJson, "--ticket-json"));
347
- }
348
- if (Object.keys(opts.ticket).length) {
349
- parts.push(opts.ticket);
350
- }
351
- if (!parts.length) return null;
352
- return Object.assign({}, ...parts);
353
- }
354
-
355
- function parseTicketJson(value, label) {
356
- try {
357
- const parsed = JSON.parse(value);
358
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
359
- die(`${label}: must be a JSON object`);
360
- }
361
- return parsed;
362
- } catch (err) {
363
- die(`${label}: failed to parse JSON (${err instanceof Error ? err.message : err})`);
364
- }
365
- }
366
-
367
- function readOptionalFile(filePath, label) {
368
- if (!filePath) return null;
369
- try {
370
- return fs.readFileSync(filePath, "utf8");
371
- } catch (err) {
372
- die(`failed to read ${label} file ${filePath}: ${err instanceof Error ? err.message : err}`);
373
- }
374
- }
375
-
376
- function resolveWorkspaceBaseUrl(explicit) {
377
- return (
378
- explicit ||
379
- process.env.SHIP_WORKSPACE_API_BASE ||
380
- process.env.SHIP_API_BASE ||
381
- "https://api.ship.elmundi.com"
382
- );
383
- }
384
-
385
- function die(message) {
386
- console.error(message);
387
- process.exit(1);
388
- }
@@ -1,43 +0,0 @@
1
- import { apiPost } from "../http.mjs";
2
-
3
- /**
4
- * Vector search over methodology corpus (documentation, prompts, README).
5
- * @param {{ baseUrl: string; json: boolean }} ctx
6
- * @param {string[]} args query tokens (same line as `ship search …`)
7
- */
8
- export async function searchCommand(ctx, args) {
9
- if (!args.length || args[0] === "help" || args[0] === "-h" || args[0] === "--help") {
10
- console.log(`Usage:
11
- shipctl search <query> [--top-k 8]
12
-
13
- POST /search on the methodology API (same SHIP_API_BASE as shipctl pattern/tool/…).
14
-
15
- Global flags: --base-url URL --json`);
16
- return;
17
- }
18
-
19
- const qParts = [];
20
- let topK = 8;
21
- for (let i = 0; i < args.length; i++) {
22
- const a = args[i];
23
- if (a === "--top-k" && args[i + 1]) {
24
- topK = Number(args[++i]);
25
- continue;
26
- }
27
- qParts.push(a);
28
- }
29
- const query = qParts.join(" ").trim();
30
- if (query.length < 3) {
31
- console.error("search: query must be at least 3 characters.");
32
- process.exit(1);
33
- }
34
- const data = await apiPost(ctx.baseUrl, "/search", { query, top_k: topK });
35
- if (ctx.json) console.log(JSON.stringify(data, null, 2));
36
- else {
37
- console.log(`Query: ${data.query}\n`);
38
- for (const r of data.results || []) {
39
- console.log(`- ${r.path} (chunk ${r.chunk_index}, distance ${r.distance ?? "n/a"})`);
40
- console.log(` ${r.snippet}\n`);
41
- }
42
- }
43
- }