@elmundi/ship-cli 0.14.2 → 0.15.4
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 +298 -119
- package/lib/commands/trigger.mjs +95 -10
- package/lib/config/schema.mjs +73 -11
- package/lib/http.mjs +0 -2
- package/lib/runtime/routines.mjs +39 -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/trigger.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readConfig } from "../config/io.mjs";
|
|
2
2
|
import { dueLanesFromRoutines, dueRoutines } from "../runtime/routines.mjs";
|
|
3
3
|
|
|
4
|
-
const VERSION = "
|
|
4
|
+
const VERSION = "v3";
|
|
5
5
|
|
|
6
6
|
export async function triggerCommand(ctx, rest) {
|
|
7
7
|
const opts = parseArgs(rest);
|
|
@@ -12,10 +12,23 @@ export async function triggerCommand(ctx, rest) {
|
|
|
12
12
|
const baseUrl = resolveBaseUrl(opts.baseUrl || explicitGlobalBaseUrl(ctx));
|
|
13
13
|
const token = process.env.SHIP_API_TOKEN || "";
|
|
14
14
|
let claimStatus = "skipped:no-token";
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let workspaceId = null;
|
|
16
|
+
let repoId = null;
|
|
17
|
+
if (token && (due.length > 0 || opts.pipelineFallback) && !opts.noClaim) {
|
|
18
|
+
// ``SHIP_WORKSPACE_ID`` env var matches what ``shipctl run`` already
|
|
19
|
+
// honours (run.mjs reads it directly). Without this fallback the
|
|
20
|
+
// CLI burned a ``GET /v1/workspaces`` round-trip every tick and
|
|
21
|
+
// — if the token's user happened to have zero memberships at that
|
|
22
|
+
// moment — printed "No workspaces visible to this token" and
|
|
23
|
+
// failed the entire schedule, even though the workflow file had
|
|
24
|
+
// the workspace ID right there in env. Honour the env var so the
|
|
25
|
+
// common case skips the discovery call entirely.
|
|
26
|
+
workspaceId =
|
|
27
|
+
opts.workspace || (process.env.SHIP_WORKSPACE_ID || "").trim() || "";
|
|
17
28
|
if (!workspaceId) workspaceId = await resolveSoleWorkspace(baseUrl, token);
|
|
18
|
-
|
|
29
|
+
repoId = await resolveRepoId(baseUrl, token, workspaceId, opts.repo);
|
|
30
|
+
}
|
|
31
|
+
if (token && due.length > 0 && !opts.noClaim) {
|
|
19
32
|
const claimed = [];
|
|
20
33
|
for (const routine of due) {
|
|
21
34
|
const claim = await claimRoutine(baseUrl, token, workspaceId, repoId, opts.event, routine);
|
|
@@ -27,6 +40,26 @@ export async function triggerCommand(ctx, rest) {
|
|
|
27
40
|
claimStatus = "attempted";
|
|
28
41
|
}
|
|
29
42
|
|
|
43
|
+
// One-action-per-tick: if any routine is due, the workflow runs the
|
|
44
|
+
// first one and exits. The pipeline-pick fallback only fires when
|
|
45
|
+
// *no* cron routine is due AND the caller asked for it (the trigger
|
|
46
|
+
// workflow does; ad-hoc CLI invocations don't, so they keep the old
|
|
47
|
+
// behaviour).
|
|
48
|
+
let nextAction = { kind: "noop" };
|
|
49
|
+
if (due.length > 0) {
|
|
50
|
+
const first = due[0];
|
|
51
|
+
nextAction = {
|
|
52
|
+
kind: "routine",
|
|
53
|
+
routine_id: first.routine_id,
|
|
54
|
+
window_key: first.window_key,
|
|
55
|
+
};
|
|
56
|
+
} else if (opts.pipelineFallback && token) {
|
|
57
|
+
const pick = await fetchPipelinePick(baseUrl, token, workspaceId, repoId, opts.event);
|
|
58
|
+
if (pick && pick.action === "pipeline_pick" && pick.specialist) {
|
|
59
|
+
nextAction = { kind: "pipeline_pick", specialist: pick.specialist };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
const result = {
|
|
31
64
|
event: opts.event,
|
|
32
65
|
status: due.length ? "due" : "noop",
|
|
@@ -34,18 +67,22 @@ export async function triggerCommand(ctx, rest) {
|
|
|
34
67
|
due_lanes: dueLanesFromRoutines(due),
|
|
35
68
|
skipped_routines: local.skipped,
|
|
36
69
|
claim_status: claimStatus,
|
|
70
|
+
next_action: nextAction,
|
|
37
71
|
};
|
|
38
72
|
|
|
39
73
|
if (ctx.json || opts.json) {
|
|
40
74
|
console.log(JSON.stringify(result, null, 2));
|
|
41
75
|
return;
|
|
42
76
|
}
|
|
43
|
-
if (
|
|
44
|
-
console.log(`Ship trigger ${opts.event}: no
|
|
77
|
+
if (nextAction.kind === "noop") {
|
|
78
|
+
console.log(`Ship trigger ${opts.event}: no action this tick.`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (nextAction.kind === "routine") {
|
|
82
|
+
console.log(`Ship trigger ${opts.event}: routine ${nextAction.routine_id}`);
|
|
45
83
|
return;
|
|
46
84
|
}
|
|
47
|
-
console.log(`Ship trigger ${opts.event}: ${
|
|
48
|
-
for (const routine of due) console.log(` - ${routine.routine_id}`);
|
|
85
|
+
console.log(`Ship trigger ${opts.event}: pipeline pick → ${nextAction.specialist}`);
|
|
49
86
|
}
|
|
50
87
|
|
|
51
88
|
function explicitGlobalBaseUrl(ctx) {
|
|
@@ -53,13 +90,30 @@ function explicitGlobalBaseUrl(ctx) {
|
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
function printHelp() {
|
|
56
|
-
console.log(`shipctl trigger — compute
|
|
93
|
+
console.log(`shipctl trigger — compute the single next Ship action for this tick (${VERSION})
|
|
57
94
|
|
|
58
95
|
USAGE
|
|
59
|
-
shipctl trigger --event schedule [--repo <id|owner/name>] [--workspace <id>]
|
|
96
|
+
shipctl trigger --event schedule [--repo <id|owner/name>] [--workspace <id>]
|
|
97
|
+
[--pipeline-fallback] [--json]
|
|
98
|
+
|
|
99
|
+
OUTPUT
|
|
100
|
+
'next_action' carries the chosen work for this tick:
|
|
101
|
+
{"kind": "routine", "routine_id": ...} — a cron routine fired this tick
|
|
102
|
+
{"kind": "pipeline_pick", "specialist": ...} — no routine due; pipeline-pick chose a specialist
|
|
103
|
+
{"kind": "noop"} — nothing to do (and no fallback, or fallback empty)
|
|
104
|
+
|
|
105
|
+
'due_routines' keeps the legacy multi-item shape for callers that
|
|
106
|
+
parse it directly; the trigger workflow only consumes 'next_action'.
|
|
107
|
+
|
|
108
|
+
FLAGS
|
|
109
|
+
--pipeline-fallback When no routine is due, call /pipeline-pick to
|
|
110
|
+
get a specialist to run instead. Default: off
|
|
111
|
+
(so ad-hoc CLI invocations don't write audit
|
|
112
|
+
log noise).
|
|
60
113
|
|
|
61
114
|
ENV
|
|
62
115
|
SHIP_API_TOKEN Optional. When set, due routines are claimed in Ship.
|
|
116
|
+
SHIP_WORKSPACE_ID Optional. Skips the /v1/workspaces lookup if set.
|
|
63
117
|
SHIP_WORKSPACE_API_BASE Optional API base override.
|
|
64
118
|
SHIP_API_BASE Fallback API base override.
|
|
65
119
|
`);
|
|
@@ -74,6 +128,7 @@ function parseArgs(args) {
|
|
|
74
128
|
cwd: null,
|
|
75
129
|
now: null,
|
|
76
130
|
noClaim: false,
|
|
131
|
+
pipelineFallback: false,
|
|
77
132
|
json: false,
|
|
78
133
|
};
|
|
79
134
|
const copy = [...args];
|
|
@@ -107,6 +162,11 @@ function parseArgs(args) {
|
|
|
107
162
|
copy.shift();
|
|
108
163
|
continue;
|
|
109
164
|
}
|
|
165
|
+
if (copy[0] === "--pipeline-fallback") {
|
|
166
|
+
out.pipelineFallback = true;
|
|
167
|
+
copy.shift();
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
110
170
|
if (copy[0] === "--json") {
|
|
111
171
|
out.json = true;
|
|
112
172
|
copy.shift();
|
|
@@ -176,6 +236,31 @@ async function apiPostJson(baseUrl, path, body, token) {
|
|
|
176
236
|
return apiRequest(baseUrl, path, "POST", token, body);
|
|
177
237
|
}
|
|
178
238
|
|
|
239
|
+
async function fetchPipelinePick(baseUrl, token, workspaceId, repoId, event) {
|
|
240
|
+
try {
|
|
241
|
+
return await apiPostJson(
|
|
242
|
+
baseUrl,
|
|
243
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/repos/${encodeURIComponent(repoId)}/pipeline-pick`,
|
|
244
|
+
{
|
|
245
|
+
event,
|
|
246
|
+
github: {
|
|
247
|
+
event_name: process.env.SHIP_EVENT_NAME || process.env.GITHUB_EVENT_NAME || "",
|
|
248
|
+
ref: process.env.SHIP_REF || process.env.GITHUB_REF || "",
|
|
249
|
+
sha: process.env.SHIP_SHA || process.env.GITHUB_SHA || "",
|
|
250
|
+
run_id: process.env.GITHUB_RUN_ID || "",
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
token,
|
|
254
|
+
);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.error(
|
|
257
|
+
`warn: pipeline-pick failed, no fallback this tick: ${err instanceof Error ? err.message : err}`,
|
|
258
|
+
);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
179
264
|
async function claimRoutine(baseUrl, token, workspaceId, repoId, event, routine) {
|
|
180
265
|
try {
|
|
181
266
|
return await apiPostJson(
|
package/lib/config/schema.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { KNOWN_AGENTS } from "../detect.mjs";
|
|
2
2
|
|
|
3
3
|
/* Historical schema used by every released shipctl through 0.11.x. We
|
|
4
|
-
* keep validating it in parallel with v2 so
|
|
5
|
-
*
|
|
4
|
+
* keep validating it in parallel with v2 so legacy configs surface a
|
|
5
|
+
* deprecation warning instead of failing silently. */
|
|
6
6
|
export const LEGACY_CONFIG_SCHEMA_VERSION = 1;
|
|
7
7
|
|
|
8
8
|
/* RFC-0007 lanes-as-config. Introduced alongside `shipctl run`. Clients
|
|
9
9
|
* that understand only v1 will refuse to read v2 and print a shipctl
|
|
10
10
|
* upgrade hint; clients that understand v2 accept v1 with a deprecation
|
|
11
|
-
* warning
|
|
11
|
+
* warning. */
|
|
12
12
|
export const CONFIG_SCHEMA_VERSION = 2;
|
|
13
13
|
|
|
14
14
|
export const SUPPORTED_CONFIG_VERSIONS = Object.freeze([
|
|
@@ -65,6 +65,12 @@ export const PROCESS_TRIGGER_TYPES = Object.freeze(["manual", "event", "schedule
|
|
|
65
65
|
* file names (`.github/workflows/ship-<lane>.yml`), and env vars, so
|
|
66
66
|
* restrict them conservatively: ASCII lowercase, digits, dash, underscore. */
|
|
67
67
|
export const LANE_ID_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
68
|
+
|
|
69
|
+
/* Agent role slug — kebab-case only (Phase 2.4). Mirrors the
|
|
70
|
+
* server's ``backend/app/services/agent_roles.is_valid_slug`` regex
|
|
71
|
+
* so an invalid slug fails locally before the runtime resolver
|
|
72
|
+
* round-trips to the workspace API. Used for ``routine.specialist``. */
|
|
73
|
+
export const AGENT_ROLE_SLUG_REGEX = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
68
74
|
export const IDEMPOTENCY_KEY_REGEX = /^[a-z0-9][a-z0-9_.-]{0,127}$/;
|
|
69
75
|
/* Coarse sanity check for 5-field crons: we're not a cron parser, but
|
|
70
76
|
* anything that isn't whitespace-separated 5 tokens is almost certainly
|
|
@@ -115,11 +121,11 @@ export const PRESETS = Object.freeze([
|
|
|
115
121
|
|
|
116
122
|
export const CHANNELS = Object.freeze(["stable", "edge"]);
|
|
117
123
|
|
|
118
|
-
export const KINDS = Object.freeze(["
|
|
124
|
+
export const KINDS = Object.freeze(["collection"]);
|
|
119
125
|
|
|
120
126
|
export const AGENT_IDS = Object.freeze(Object.keys(KNOWN_AGENTS));
|
|
121
127
|
|
|
122
|
-
export const PIN_KEY_REGEX = /^
|
|
128
|
+
export const PIN_KEY_REGEX = /^collection\/[a-zA-Z0-9_\-\.\/]+$/;
|
|
123
129
|
|
|
124
130
|
export const UUID_V4_REGEX =
|
|
125
131
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -330,7 +336,16 @@ function validateV2Process(obj, errors, warnings) {
|
|
|
330
336
|
}
|
|
331
337
|
pushUnknownKeyWarnings(
|
|
332
338
|
process,
|
|
333
|
-
new Set([
|
|
339
|
+
new Set([
|
|
340
|
+
"id",
|
|
341
|
+
"name",
|
|
342
|
+
"primary",
|
|
343
|
+
"states",
|
|
344
|
+
"transitions",
|
|
345
|
+
"routines",
|
|
346
|
+
"schedule",
|
|
347
|
+
"gates",
|
|
348
|
+
]),
|
|
334
349
|
"process",
|
|
335
350
|
warnings,
|
|
336
351
|
);
|
|
@@ -343,6 +358,19 @@ function validateV2Process(obj, errors, warnings) {
|
|
|
343
358
|
if (process.primary !== undefined && typeof process.primary !== "boolean") {
|
|
344
359
|
errors.push("process.primary: must be boolean when set");
|
|
345
360
|
}
|
|
361
|
+
// Where the operator wants to interject (Phase 3). ``after_pr``
|
|
362
|
+
// (default) is fully autonomous through the agent reviewer;
|
|
363
|
+
// ``after_arch`` pauses for human approval after architects;
|
|
364
|
+
// ``after_ba`` pauses earlier, after BA writes the spec.
|
|
365
|
+
// Enforcement (FSM "needs review" routing) lands in Phase 3.5.
|
|
366
|
+
if (process.gates !== undefined) {
|
|
367
|
+
const allowed = new Set(["after_ba", "after_arch", "after_pr"]);
|
|
368
|
+
if (typeof process.gates !== "string" || !allowed.has(process.gates)) {
|
|
369
|
+
errors.push(
|
|
370
|
+
`process.gates: must be one of ${[...allowed].join(" | ")} when set`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
346
374
|
|
|
347
375
|
if (!Array.isArray(process.states) || process.states.length < 1) {
|
|
348
376
|
errors.push("process.states: must contain at least one state");
|
|
@@ -530,6 +558,10 @@ function validateProcessRoutines(value, errors, warnings) {
|
|
|
530
558
|
"schedule",
|
|
531
559
|
"window",
|
|
532
560
|
"event",
|
|
561
|
+
// ``fsm_stage`` overrides the role's default stage so one role
|
|
562
|
+
// can drive multiple processes (BA serves both
|
|
563
|
+
// ``ba_requirements`` for SDLC and ``wbs`` for decomposition).
|
|
564
|
+
"fsm_stage",
|
|
533
565
|
]),
|
|
534
566
|
prefix,
|
|
535
567
|
warnings,
|
|
@@ -558,7 +590,37 @@ function validateProcessRoutines(value, errors, warnings) {
|
|
|
558
590
|
}
|
|
559
591
|
requireOptionalString(routine.specialist_id, `${prefix}.specialist_id`, errors, { required: false });
|
|
560
592
|
requireOptionalString(routine.specialist_name, `${prefix}.specialist_name`, errors, { required: false });
|
|
593
|
+
/* ``routine.specialist`` is the canonical Phase 2.4 form — an
|
|
594
|
+
* agent role slug ``shipctl run`` resolves through
|
|
595
|
+
* ``GET /v1/.../agent-roles/{slug}/resolve``. Two shapes are
|
|
596
|
+
* accepted: a bare string (the slug) for the new spec, and an
|
|
597
|
+
* object (with ``id`` / ``name``) for the legacy process-state
|
|
598
|
+
* specialist record. Object form is validated elsewhere; here we
|
|
599
|
+
* only police the string form's slug grammar. */
|
|
600
|
+
if (typeof routine.specialist === "string") {
|
|
601
|
+
if (!AGENT_ROLE_SLUG_REGEX.test(routine.specialist)) {
|
|
602
|
+
errors.push(
|
|
603
|
+
`${prefix}.specialist: must be an agent-role slug (kebab-case [a-z0-9-]{1,64})`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/* Legacy ``pattern: <slug>`` keeps working but nudges authors
|
|
608
|
+
* toward the new vocabulary. ``specialist: <slug>`` wins when
|
|
609
|
+
* both are set. */
|
|
610
|
+
if (
|
|
611
|
+
typeof routine.pattern === "string" &&
|
|
612
|
+
routine.pattern.trim() &&
|
|
613
|
+
typeof routine.specialist !== "string"
|
|
614
|
+
) {
|
|
615
|
+
const suggested = routine.pattern.startsWith("role-")
|
|
616
|
+
? routine.pattern.slice("role-".length)
|
|
617
|
+
: routine.pattern;
|
|
618
|
+
warnings.push(
|
|
619
|
+
`${prefix}.pattern: deprecated alias — use \`specialist: ${suggested}\` instead`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
561
622
|
validateProcessAgentProfile(routine.agent_profile, `${prefix}.agent_profile`, errors);
|
|
623
|
+
requireOptionalString(routine.fsm_stage, `${prefix}.fsm_stage`, errors, { required: false });
|
|
562
624
|
validateRoutineTrigger(routine.trigger, `${prefix}.trigger`, errors);
|
|
563
625
|
if (routine.schedule !== undefined && routine.schedule !== null) {
|
|
564
626
|
if (typeof routine.schedule !== "string" && !isPlainObject(routine.schedule)) {
|
|
@@ -639,9 +701,9 @@ function validateV2Lanes(obj, errors, warnings) {
|
|
|
639
701
|
|
|
640
702
|
const lanes = obj.lanes;
|
|
641
703
|
if (lanes === undefined) {
|
|
642
|
-
/* An empty lanes map is legal — a fresh repo
|
|
643
|
-
*
|
|
644
|
-
*
|
|
704
|
+
/* An empty lanes map is legal — a fresh repo seeded by the wizard
|
|
705
|
+
* declares automation under `process.routines:` instead, and the
|
|
706
|
+
* top-level `lanes:` key is left out. */
|
|
645
707
|
return;
|
|
646
708
|
}
|
|
647
709
|
if (!isPlainObject(lanes)) {
|
|
@@ -863,7 +925,7 @@ export function validateConfig(obj) {
|
|
|
863
925
|
|
|
864
926
|
if (!isV2) {
|
|
865
927
|
warnings.push(
|
|
866
|
-
`version: config is at v${obj.version};
|
|
928
|
+
`version: config is at v${obj.version}; v${CONFIG_SCHEMA_VERSION} is the current schema. Re-seed the repo from the workspace wizard to upgrade.`,
|
|
867
929
|
);
|
|
868
930
|
}
|
|
869
931
|
|
|
@@ -944,7 +1006,7 @@ export function validateConfig(obj) {
|
|
|
944
1006
|
for (const [k, v] of Object.entries(artifacts.pins)) {
|
|
945
1007
|
if (!PIN_KEY_REGEX.test(k)) {
|
|
946
1008
|
errors.push(
|
|
947
|
-
`artifacts.pins[${JSON.stringify(k)}]: invalid key; expected
|
|
1009
|
+
`artifacts.pins[${JSON.stringify(k)}]: invalid key; expected collection/<id>`,
|
|
948
1010
|
);
|
|
949
1011
|
}
|
|
950
1012
|
if (typeof v !== "string" || !SEMVER_OR_RANGE_REGEX.test(v.trim())) {
|
package/lib/http.mjs
CHANGED
|
@@ -93,8 +93,6 @@ export async function apiGet(baseUrl, path) {
|
|
|
93
93
|
*/
|
|
94
94
|
export async function fetchManifest(baseUrl, { channel } = {}) {
|
|
95
95
|
const KINDS = [
|
|
96
|
-
{ plural: "patterns", singular: "pattern" },
|
|
97
|
-
{ plural: "tools", singular: "tool" },
|
|
98
96
|
{ plural: "collections", singular: "collection" },
|
|
99
97
|
];
|
|
100
98
|
const responses = await Promise.all(
|
package/lib/runtime/routines.mjs
CHANGED
|
@@ -47,11 +47,16 @@ export function executableIds(config) {
|
|
|
47
47
|
|
|
48
48
|
export function routineToExecutable(id, routine) {
|
|
49
49
|
const trigger = normalizeRoutineTrigger(routine);
|
|
50
|
+
// Phase 2.4: ``specialist:`` is the canonical agent-role slug;
|
|
51
|
+
// ``pattern:`` is kept as a back-compat alias and translated to a
|
|
52
|
+
// slug below (drop the ``role-`` prefix the legacy catalog used).
|
|
53
|
+
const specialist = pickSpecialistSlug(routine);
|
|
50
54
|
return {
|
|
51
55
|
id,
|
|
52
56
|
type: "routine",
|
|
53
57
|
kind: trigger.kind,
|
|
54
58
|
trigger,
|
|
59
|
+
specialist,
|
|
55
60
|
pattern: stringOrNull(routine.pattern),
|
|
56
61
|
patterns: Array.isArray(routine.patterns) ? routine.patterns : undefined,
|
|
57
62
|
pattern_version: stringOrNull(routine.pattern_version),
|
|
@@ -59,9 +64,43 @@ export function routineToExecutable(id, routine) {
|
|
|
59
64
|
idempotency: routine.idempotency || null,
|
|
60
65
|
prompt: stringOrNull(routine.prompt) || stringOrNull(routine.instructions),
|
|
61
66
|
agent_profile: stringOrNull(routine.agent_profile) || stringOrNull(routine.specialist?.agent_profile),
|
|
67
|
+
// Per-routine FSM stage override. When set, ``shipctl run`` uses
|
|
68
|
+
// it instead of the role's default ``fsm_stage`` — that's how a
|
|
69
|
+
// single role (e.g. ``ba``) serves both SDLC (``ba_requirements``)
|
|
70
|
+
// and decomposition (``wbs``) without per-process role clones.
|
|
71
|
+
fsm_stage: stringOrNull(routine.fsm_stage),
|
|
62
72
|
};
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Pull the agent-role slug out of a routine, preferring the new
|
|
77
|
+
* ``specialist:`` key but falling back to the legacy ``pattern:`` and
|
|
78
|
+
* stripping the historical ``role-`` prefix when present.
|
|
79
|
+
*
|
|
80
|
+
* Object-form ``specialist`` (the process-state record with ``id`` /
|
|
81
|
+
* ``name``) is treated as a slug source via ``specialist.id`` for
|
|
82
|
+
* configs that mirror the process-stage shape into routines.
|
|
83
|
+
*/
|
|
84
|
+
function pickSpecialistSlug(routine) {
|
|
85
|
+
if (typeof routine.specialist === "string") {
|
|
86
|
+
const v = routine.specialist.trim();
|
|
87
|
+
if (v) return v;
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
routine.specialist &&
|
|
91
|
+
typeof routine.specialist === "object" &&
|
|
92
|
+
typeof routine.specialist.id === "string"
|
|
93
|
+
) {
|
|
94
|
+
const v = routine.specialist.id.trim();
|
|
95
|
+
if (v) return v;
|
|
96
|
+
}
|
|
97
|
+
if (typeof routine.pattern === "string" && routine.pattern.trim()) {
|
|
98
|
+
const p = routine.pattern.trim();
|
|
99
|
+
return p.startsWith("role-") ? p.slice("role-".length) : p;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
65
104
|
export function laneToExecutable(id, lane) {
|
|
66
105
|
return {
|
|
67
106
|
id,
|
package/lib/templates.mjs
CHANGED
|
@@ -33,8 +33,8 @@ shipctl pattern list
|
|
|
33
33
|
shipctl pattern show role-developer # resolves latest or pin
|
|
34
34
|
shipctl pattern fetch role-developer --version 1.4.2
|
|
35
35
|
shipctl search "release gates and qa split" --top-k 8
|
|
36
|
-
shipctl
|
|
37
|
-
shipctl sync
|
|
36
|
+
shipctl knowledge fetch repository-context # workspace bucket
|
|
37
|
+
shipctl sync # reconcile .ship/cache/
|
|
38
38
|
\`\`\`
|
|
39
39
|
|
|
40
40
|
## HTTP (curl, no CLI)
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import { detectAgentTargets } from "../../detect.mjs";
|
|
4
|
-
import { readCachedArtifact } from "../../cache/store.mjs";
|
|
5
2
|
|
|
6
3
|
export const id = "agents-on-disk";
|
|
7
4
|
export const category = "config";
|
|
@@ -17,32 +14,12 @@ export async function run(ctx) {
|
|
|
17
14
|
if (!declared.length) {
|
|
18
15
|
return { status: "skip", detail: "stack.agents is empty" };
|
|
19
16
|
}
|
|
17
|
+
// Phase 2.5 retired the local artifact cache; agent-rule install
|
|
18
|
+
// paths are no longer available via cached frontmatter. Fall back
|
|
19
|
+
// to the heuristic detector only — it covers the common targets
|
|
20
|
+
// (CLAUDE.md, AGENTS.md, .cursor/rules/...) the seed PR writes.
|
|
20
21
|
const detected = new Set(detectAgentTargets(ctx.cwd).map((t) => t.id));
|
|
21
|
-
const missing =
|
|
22
|
-
for (const agent of declared) {
|
|
23
|
-
if (detected.has(agent)) continue;
|
|
24
|
-
// Second chance: the cached agent-rules artifact may declare a custom
|
|
25
|
-
// install_target (e.g. codex -> AGENTS.md) that the heuristic detector
|
|
26
|
-
// doesn't recognise. Treat a present install_target file as "signal".
|
|
27
|
-
let fm = null;
|
|
28
|
-
try {
|
|
29
|
-
fm = readCachedArtifact(ctx.cwd, "collection", `agent-rules-${agent}`);
|
|
30
|
-
} catch {
|
|
31
|
-
fm = null;
|
|
32
|
-
}
|
|
33
|
-
const topLevel = fm && fm.fm && typeof fm.fm.install_target === "string"
|
|
34
|
-
? fm.fm.install_target.trim()
|
|
35
|
-
: "";
|
|
36
|
-
const nested = fm && fm.spec && typeof fm.spec.install_target === "string"
|
|
37
|
-
? fm.spec.install_target.trim()
|
|
38
|
-
: "";
|
|
39
|
-
const target = topLevel || nested;
|
|
40
|
-
if (target && fs.existsSync(path.join(ctx.cwd, target))) {
|
|
41
|
-
detected.add(agent);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
missing.push(agent);
|
|
45
|
-
}
|
|
22
|
+
const missing = declared.filter((agent) => !detected.has(agent));
|
|
46
23
|
if (missing.length) {
|
|
47
24
|
return {
|
|
48
25
|
status: "warn",
|
package/lib/verify/registry.mjs
CHANGED
|
@@ -23,32 +23,31 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import * as configPresent from "./checks/config-present.mjs";
|
|
26
|
-
import * as gitignoreCache from "./checks/gitignore-cache.mjs";
|
|
27
|
-
import * as rulesMarkers from "./checks/rules-markers.mjs";
|
|
28
|
-
import * as cacheIntegrity from "./checks/cache-integrity.mjs";
|
|
29
26
|
import * as bootstrapFiles from "./checks/bootstrap-files.mjs";
|
|
30
27
|
import * as stackEnums from "./checks/stack-enums.mjs";
|
|
31
28
|
import * as agentsOnDisk from "./checks/agents-on-disk.mjs";
|
|
32
29
|
import * as apiReachable from "./checks/api-reachable.mjs";
|
|
33
|
-
import * as artifactsUpToDate from "./checks/artifacts-up-to-date.mjs";
|
|
34
30
|
import * as trackerLabels from "./checks/tracker-labels.mjs";
|
|
35
31
|
import * as ciSecrets from "./checks/ci-secrets.mjs";
|
|
36
32
|
|
|
37
33
|
/**
|
|
38
34
|
* Ordered list of checks. Order governs how they appear in `shipctl verify`
|
|
39
35
|
* output; within a category we keep a stable human-friendly grouping.
|
|
36
|
+
*
|
|
37
|
+
* Phase 2.5 retired four cache-coupled checks (``gitignore-cache``,
|
|
38
|
+
* ``rules-markers``, ``cache-integrity``, ``artifacts-up-to-date``)
|
|
39
|
+
* along with the ``shipctl sync`` flow that filled the cache. Agent
|
|
40
|
+
* rule files now live in the seed PR — there's nothing left in
|
|
41
|
+
* ``.ship/cache`` for ``verify`` to validate.
|
|
42
|
+
*
|
|
40
43
|
* @type {Check[]}
|
|
41
44
|
*/
|
|
42
45
|
const CHECKS = [
|
|
43
46
|
configPresent,
|
|
44
|
-
gitignoreCache,
|
|
45
47
|
stackEnums,
|
|
46
|
-
rulesMarkers,
|
|
47
|
-
cacheIntegrity,
|
|
48
48
|
bootstrapFiles,
|
|
49
49
|
agentsOnDisk,
|
|
50
50
|
apiReachable,
|
|
51
|
-
artifactsUpToDate,
|
|
52
51
|
trackerLabels,
|
|
53
52
|
ciSecrets,
|
|
54
53
|
];
|