@sechroom/cli 2026.6.29 → 2026.6.30

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 (2) hide show
  1. package/dist/index.js +363 -216
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1780,7 +1780,7 @@ import { delimiter, dirname as dirname4, join as join4 } from "path";
1780
1780
 
1781
1781
  // src/sem.ts
1782
1782
  import { basename as basename2, dirname as dirname2, join as join2 } from "path";
1783
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1783
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
1784
1784
  var SEM_FILE = join2(".sechroom", "lane.json");
1785
1785
  var LEGACY_SEM_FILE = ".sem";
1786
1786
  var STATE_DIR_NAME2 = ".sechroom";
@@ -1799,6 +1799,43 @@ function resolveSemPathForRead(start = process.cwd()) {
1799
1799
  dir = parent;
1800
1800
  }
1801
1801
  }
1802
+ function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
1803
+ try {
1804
+ let dir = start;
1805
+ let gitPath;
1806
+ for (; ; ) {
1807
+ const candidate = join2(dir, ".git");
1808
+ if (existsSync2(candidate)) {
1809
+ gitPath = candidate;
1810
+ break;
1811
+ }
1812
+ const parent = dirname2(dir);
1813
+ if (parent === dir) break;
1814
+ dir = parent;
1815
+ }
1816
+ if (!gitPath || statSync(gitPath).isDirectory()) return lane;
1817
+ const gitFile = readFileSync2(gitPath, "utf8");
1818
+ const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
1819
+ if (!common) return lane;
1820
+ const worktreesDir = join2(common[1], "worktrees");
1821
+ const siblings = readdirSync(worktreesDir).filter((n) => {
1822
+ try {
1823
+ return statSync(join2(worktreesDir, n)).isDirectory();
1824
+ } catch {
1825
+ return false;
1826
+ }
1827
+ });
1828
+ return laneWithWorktreeSuffix(lane, gitFile, siblings);
1829
+ } catch {
1830
+ return lane;
1831
+ }
1832
+ }
1833
+ function laneWithWorktreeSuffix(lane, gitFile, siblings) {
1834
+ const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
1835
+ if (!m) return lane;
1836
+ const idx = [...siblings].sort().indexOf(m[1]);
1837
+ return idx < 0 ? lane : `${lane}-${idx + 2}`;
1838
+ }
1802
1839
  function parseSem(text) {
1803
1840
  const out = {};
1804
1841
  for (const raw of text.split("\n")) {
@@ -2090,8 +2127,9 @@ function resolveLane(flagLane, cwd) {
2090
2127
  const env = process.env.SECHROOM_LANE;
2091
2128
  if (env) return env;
2092
2129
  const start = cwd ?? process.cwd();
2093
- const sem = readSem(resolveSemPathForRead(start));
2094
- return sem?.values["code-lane"];
2130
+ const base = readSem(resolveSemPathForRead(start))?.values["code-lane"];
2131
+ if (!base) return void 0;
2132
+ return applyWorktreeLaneSuffix(base, start);
2095
2133
  }
2096
2134
  var INTENT_FILE = join4(".sechroom", "continuity.json");
2097
2135
  function resolveIntentPath(start) {
@@ -2859,9 +2897,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
2859
2897
  }
2860
2898
 
2861
2899
  // src/setup/skills-offer.ts
2862
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
2863
- import { homedir as homedir5 } from "os";
2864
- import { join as join5 } from "path";
2900
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
2901
+ import { join as join6 } from "path";
2865
2902
 
2866
2903
  // src/setup/lane-pin.ts
2867
2904
  var CODE_LANE_PREFIX_BY_CLIENT = {
@@ -2943,57 +2980,157 @@ I can pin this checkout's lane so operator skills + the continuity hook resolve
2943
2980
  writePin(code || void 0, design || void 0);
2944
2981
  }
2945
2982
 
2946
- // src/setup/skills-offer.ts
2947
- var ROLE_TAG = "sechroom:role:skill-template";
2983
+ // src/setup/skill-resolution.ts
2984
+ var SYSTEM_WORKSPACE_ID = "wsp_system";
2985
+ var SKILL_ROLE_TAG = "sechroom:role:skill-template";
2986
+ var SKILL_NAME_PREFIX = "skill:";
2987
+ var AGENT_ROLE_TAG = "sechroom:role:agent-template";
2988
+ var AGENT_NAME_PREFIX = "agent:";
2989
+ function tagsOf(row) {
2990
+ const m = row?.item ?? row;
2991
+ return m?.tags ?? m?.Tags ?? [];
2992
+ }
2948
2993
  function tagValue(tags, prefix) {
2949
2994
  return tags.find((t) => t.startsWith(prefix))?.slice(prefix.length);
2950
2995
  }
2951
- async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
2952
- if (!personalWorkspaceId || opts.dryRun) return;
2953
- const surface = opts.surface ?? "claude-code";
2954
- let rows = [];
2996
+ function bodyOf(row) {
2997
+ const m = row?.item ?? row;
2998
+ return m?.text ?? m?.Text ?? "";
2999
+ }
3000
+ function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
3001
+ const out = /* @__PURE__ */ new Map();
3002
+ for (const row of rows ?? []) {
3003
+ const tags = tagsOf(row);
3004
+ if (!tags.includes(roleTag)) continue;
3005
+ if (tagValue(tags, "target:") !== surface) continue;
3006
+ const name = tagValue(tags, namePrefix);
3007
+ if (!name) continue;
3008
+ out.set(name, { name, body: bodyOf(row), source });
3009
+ }
3010
+ return out;
3011
+ }
3012
+ function resolveByRole(systemRows, personalRows, surface, roleTag, namePrefix) {
3013
+ const merged = entriesFromRows(systemRows, surface, "system", roleTag, namePrefix);
3014
+ for (const [name, item] of entriesFromRows(personalRows, surface, "personal", roleTag, namePrefix)) {
3015
+ merged.set(name, item);
3016
+ }
3017
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
3018
+ }
3019
+ function resolveSkills(systemRows, personalRows, surface) {
3020
+ return resolveByRole(systemRows, personalRows, surface, SKILL_ROLE_TAG, SKILL_NAME_PREFIX);
3021
+ }
3022
+ function resolveAgents(systemRows, personalRows, surface) {
3023
+ return resolveByRole(systemRows, personalRows, surface, AGENT_ROLE_TAG, AGENT_NAME_PREFIX);
3024
+ }
3025
+
3026
+ // src/setup/skills-lock.ts
3027
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
3028
+ import { homedir as homedir5 } from "os";
3029
+ import { join as join5 } from "path";
3030
+ var SKILLS_LOCK = ".sechroom-skills.json";
3031
+ var DEFAULT_SKILLS_SLUG = "operator-skills";
3032
+ function skillsDir(global) {
3033
+ return global ? join5(homedir5(), ".claude", "skills") : join5(process.cwd(), ".claude", "skills");
3034
+ }
3035
+ function agentsDir(global) {
3036
+ return global ? join5(homedir5(), ".claude", "agents") : join5(process.cwd(), ".claude", "agents");
3037
+ }
3038
+ function readSkillsLock(dir) {
3039
+ const lockPath = join5(dir, SKILLS_LOCK);
3040
+ if (!existsSync6(lockPath)) return {};
3041
+ try {
3042
+ return JSON.parse(readFileSync5(lockPath, "utf8"));
3043
+ } catch {
3044
+ return {};
3045
+ }
3046
+ }
3047
+ function writeSkillsLock(dir, lock) {
3048
+ mkdirSync5(dir, { recursive: true });
3049
+ writeFileSync5(join5(dir, SKILLS_LOCK), JSON.stringify(lock, null, 2) + "\n");
3050
+ }
3051
+ function recordMaterialisedSkills(dir, slug, skills, meta = {}) {
3052
+ const lock = readSkillsLock(dir);
3053
+ lock[slug] = { surface: meta.surface, skills: [...skills].sort() };
3054
+ writeSkillsLock(dir, lock);
3055
+ }
3056
+
3057
+ // src/setup/skills-offer.ts
3058
+ async function fetchFeedRows(cfg, workspaceId) {
2955
3059
  try {
2956
3060
  const client = await makeClient(cfg);
2957
3061
  const feed = await client.GET("/workspaces/{workspaceId}/memories/feed", {
2958
3062
  params: {
2959
- path: { workspaceId: personalWorkspaceId },
3063
+ path: { workspaceId },
3064
+ // cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
3065
+ // includeText: the feed omits bodies by default, we need them for SKILL.md.
2960
3066
  query: { limit: 200, cascadeWorkspaces: true, includeText: true }
2961
3067
  }
2962
3068
  }).then((r) => r.data).catch(() => void 0);
2963
- rows = feed?.results ?? feed?.Results ?? [];
3069
+ return feed?.results ?? feed?.Results ?? [];
2964
3070
  } catch {
2965
- return;
3071
+ return [];
2966
3072
  }
2967
- const skills = rows.map((r) => r.item ?? r).filter((m) => {
2968
- const tags = m.tags ?? m.Tags ?? [];
2969
- return tags.includes(ROLE_TAG) && tagValue(tags, "target:") === surface;
2970
- });
2971
- if (skills.length === 0) return;
2972
- const byName = /* @__PURE__ */ new Map();
2973
- for (const m of skills) {
2974
- const name = tagValue(m.tags ?? m.Tags ?? [], "skill:");
2975
- if (name) byName.set(name, m);
3073
+ }
3074
+ async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
3075
+ const surface = opts.surface ?? "claude-code";
3076
+ const [systemRows, personalRows] = await Promise.all([
3077
+ fetchFeedRows(cfg, SYSTEM_WORKSPACE_ID),
3078
+ personalWorkspaceId ? fetchFeedRows(cfg, personalWorkspaceId) : Promise.resolve([])
3079
+ ]);
3080
+ const skills = resolveSkills(systemRows, personalRows, surface);
3081
+ const agents = resolveAgents(systemRows, personalRows, surface);
3082
+ if (skills.length === 0 && agents.length === 0) return;
3083
+ const sDir = skillsDir(true);
3084
+ const aDir = agentsDir(true);
3085
+ if (opts.dryRun) {
3086
+ const lines = (label, items) => items.length === 0 ? "" : `
3087
+ Would materialise ${style.bold(String(items.length))} ${label} for ${surface}:
3088
+ ` + items.map((s) => ` ${s.name} ${style.dim(`[${s.source}]`)}`).join("\n") + "\n";
3089
+ process.stderr.write(lines("operator skill(s)", skills) + lines("agent(s)", agents));
3090
+ return;
2976
3091
  }
2977
- const names = [...byName.keys()].sort();
2978
- if (names.length === 0) return;
2979
- process.stderr.write(
2980
- `
2981
- Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
2982
- `
2983
- );
2984
- const dir = join5(homedir5(), ".claude", "skills");
2985
- const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
3092
+ const summary = [
3093
+ skills.length > 0 ? `${style.bold(String(skills.length))} skill(s)` : "",
3094
+ agents.length > 0 ? `${style.bold(String(agents.length))} agent(s)` : ""
3095
+ ].filter(Boolean).join(" + ");
3096
+ process.stderr.write(`
3097
+ Found ${summary} available to you for ${surface}.
3098
+ `);
3099
+ if (skills.length > 0) process.stderr.write(` skills: ${skills.map((s) => s.name).join(", ")}
3100
+ `);
3101
+ if (agents.length > 0) process.stderr.write(` agents: ${agents.map((a) => a.name).join(", ")}
3102
+ `);
3103
+ const dest = [skills.length > 0 ? `${sDir}/` : "", agents.length > 0 ? `${aDir}/` : ""].filter(Boolean).join(" + ");
3104
+ const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dest} so ${surface} can use them?`) : false;
2986
3105
  if (!materialise) return;
2987
- const written = [];
2988
- for (const [name, m] of byName) {
2989
- const body = m.text ?? m.Text ?? "";
2990
- mkdirSync5(join5(dir, name), { recursive: true });
2991
- writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
2992
- written.push(name);
3106
+ if (skills.length > 0) {
3107
+ const written = [];
3108
+ for (const s of skills) {
3109
+ mkdirSync6(join6(sDir, s.name), { recursive: true });
3110
+ writeFileSync6(join6(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
3111
+ written.push(s.name);
3112
+ }
3113
+ recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
3114
+ process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${sDir}
3115
+ `);
2993
3116
  }
2994
- process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
3117
+ if (agents.length > 0) {
3118
+ mkdirSync6(aDir, { recursive: true });
3119
+ const written = [];
3120
+ for (const a of agents) {
3121
+ const file = `${a.name}.md`;
3122
+ writeFileSync6(join6(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
3123
+ written.push(file);
3124
+ }
3125
+ recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
3126
+ process.stderr.write(`${style.green("\u2713")} wrote ${written.length} agent(s) to ${aDir}
2995
3127
  `);
2996
- await ensureLanePin(cfg, { yes: opts.yes, dryRun: opts.dryRun, clients: [surface] });
3128
+ }
3129
+ await ensureLanePin(cfg, {
3130
+ yes: opts.yes,
3131
+ dryRun: opts.dryRun,
3132
+ clients: [surface]
3133
+ });
2997
3134
  }
2998
3135
 
2999
3136
  // src/commands/setup.ts
@@ -3144,13 +3281,13 @@ async function runClients(clients, cmd, opts) {
3144
3281
  }
3145
3282
 
3146
3283
  // src/commands/onboard.ts
3147
- import { existsSync as existsSync7 } from "fs";
3148
- import { join as join7 } from "path";
3284
+ import { existsSync as existsSync8 } from "fs";
3285
+ import { basename as basename3, join as join8 } from "path";
3149
3286
 
3150
3287
  // src/commands/fanout.ts
3151
3288
  import { spawnSync } from "child_process";
3152
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync, statSync } from "fs";
3153
- import { isAbsolute, join as join6, resolve } from "path";
3289
+ import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
3290
+ import { isAbsolute, join as join7, resolve } from "path";
3154
3291
  var ICON = {
3155
3292
  refresh: "\u21BB",
3156
3293
  bind: "+",
@@ -3163,28 +3300,28 @@ function resolveChildDir(path, root) {
3163
3300
  function discoverChildren(root) {
3164
3301
  let names;
3165
3302
  try {
3166
- names = readdirSync(root);
3303
+ names = readdirSync2(root);
3167
3304
  } catch {
3168
3305
  return [];
3169
3306
  }
3170
3307
  const out = [];
3171
3308
  for (const name of names.sort()) {
3172
3309
  if (name.startsWith(".") || name === "node_modules") continue;
3173
- const dir = join6(root, name);
3310
+ const dir = join7(root, name);
3174
3311
  try {
3175
- if (!statSync(dir).isDirectory()) continue;
3312
+ if (!statSync2(dir).isDirectory()) continue;
3176
3313
  } catch {
3177
3314
  continue;
3178
3315
  }
3179
- if (existsSync6(join6(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3316
+ if (existsSync7(join7(dir, ".git")) || committedBindingPath(dir)) out.push(name);
3180
3317
  }
3181
3318
  return out;
3182
3319
  }
3183
3320
  function readManifest(path) {
3184
- if (!existsSync6(path)) return null;
3321
+ if (!existsSync7(path)) return null;
3185
3322
  let parsed;
3186
3323
  try {
3187
- parsed = JSON.parse(readFileSync5(path, "utf8"));
3324
+ parsed = JSON.parse(readFileSync6(path, "utf8"));
3188
3325
  } catch (err2) {
3189
3326
  throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
3190
3327
  }
@@ -3322,7 +3459,39 @@ async function warnIfProjectStray(client, projectId, workspaceId, json) {
3322
3459
  );
3323
3460
  }
3324
3461
  }
3325
- async function pickWorkspace(client, promptLabel = "Bind this directory to a workspace:") {
3462
+ async function fetchPersonalWorkspaceId(client) {
3463
+ try {
3464
+ const { data } = await client.GET("/me/personal-workspace", {});
3465
+ return data?.workspaceId ?? null;
3466
+ } catch {
3467
+ return null;
3468
+ }
3469
+ }
3470
+ function nameTokens(s) {
3471
+ return s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
3472
+ }
3473
+ function personalSubtreeIds(personalId, all) {
3474
+ const childrenOf = /* @__PURE__ */ new Map();
3475
+ for (const w of all) {
3476
+ if (!w.parentId) continue;
3477
+ (childrenOf.get(w.parentId) ?? childrenOf.set(w.parentId, []).get(w.parentId)).push(w);
3478
+ }
3479
+ const ids = /* @__PURE__ */ new Set([personalId]);
3480
+ const queue = [personalId];
3481
+ while (queue.length > 0) {
3482
+ const id = queue.shift();
3483
+ for (const child of childrenOf.get(id) ?? []) {
3484
+ if (!ids.has(child.id)) {
3485
+ ids.add(child.id);
3486
+ queue.push(child.id);
3487
+ }
3488
+ }
3489
+ }
3490
+ return ids;
3491
+ }
3492
+ async function pickWorkspace(client, opts = {}) {
3493
+ const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
3494
+ const dirName = opts.dirName ?? basename3(process.cwd());
3326
3495
  const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
3327
3496
  if (all.length === 0) {
3328
3497
  process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
@@ -3330,22 +3499,34 @@ async function pickWorkspace(client, promptLabel = "Bind this directory to a wor
3330
3499
  return void 0;
3331
3500
  }
3332
3501
  const byId = new Map(all.map((w) => [w.id, w]));
3333
- let pool = all;
3334
- if (all.length > 12) {
3335
- const q = (await promptText(`Filter ${all.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
3502
+ const personalId = await fetchPersonalWorkspaceId(client);
3503
+ const excluded = personalId ? personalSubtreeIds(personalId, all) : /* @__PURE__ */ new Set();
3504
+ let candidates = all.filter((w) => !excluded.has(w.id));
3505
+ if (candidates.length === 0) candidates = all;
3506
+ const dirToks = new Set(nameTokens(dirName));
3507
+ const isMatch = (w) => nameTokens(w.name).some((t) => dirToks.has(t));
3508
+ const suggestions = candidates.filter(isMatch);
3509
+ let pool = candidates;
3510
+ if (candidates.length > 12 && suggestions.length === 0) {
3511
+ const q = (await promptText(`Filter ${candidates.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
3336
3512
  if (q) {
3337
- const hits = all.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
3513
+ const hits = candidates.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
3338
3514
  if (hits.length > 0) pool = hits;
3339
3515
  else process.stderr.write(`no match for "${q}" \u2014 listing all
3340
3516
  `);
3341
3517
  }
3342
3518
  }
3343
3519
  const SKIP = "__skip__";
3520
+ const byPath = (a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId));
3521
+ const matched = pool.filter(isMatch).sort(byPath);
3522
+ const rest = pool.filter((w) => !isMatch(w)).sort(byPath);
3344
3523
  const choices = [
3345
- ...pool.slice().sort((a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId))).map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
3524
+ ...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
3525
+ ...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
3346
3526
  { label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
3347
3527
  ];
3348
- const chosen = await promptSelect(promptLabel, choices, SKIP);
3528
+ const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
3529
+ const chosen = await promptSelect(promptLabel, choices, defaultValue);
3349
3530
  if (chosen === SKIP) return void 0;
3350
3531
  const picked = byId.get(chosen);
3351
3532
  const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
@@ -3372,7 +3553,7 @@ async function resolveWorkspaceBinding(client, existing, opts) {
3372
3553
  }
3373
3554
  if (existing) return existing;
3374
3555
  if (!canPrompt() || opts.yes) return void 0;
3375
- return pickWorkspace(client);
3556
+ return pickWorkspace(client, { dirName: basename3(process.cwd()) });
3376
3557
  }
3377
3558
  async function ensureTenant(baseUrl, g, opts) {
3378
3559
  const persisted = readPersisted();
@@ -3505,10 +3686,10 @@ async function chooseClients(clientFlag, yes, cwd) {
3505
3686
  }
3506
3687
  async function planRecurseChild(entry, root, client, opts) {
3507
3688
  const dir = resolveChildDir(entry.path, root);
3508
- if (!existsSync7(dir)) {
3689
+ if (!existsSync8(dir)) {
3509
3690
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
3510
3691
  }
3511
- if (existsSync7(join7(dir, ".sechroom.json"))) {
3692
+ if (existsSync8(join8(dir, ".sechroom.json"))) {
3512
3693
  return {
3513
3694
  label: entry.path,
3514
3695
  dir,
@@ -3535,7 +3716,10 @@ async function planRecurseChild(entry, root, client, opts) {
3535
3716
  process.stderr.write(`
3536
3717
  ${style.bold(entry.path)} ${style.dim("is not bound yet.")}
3537
3718
  `);
3538
- const ws = await pickWorkspace(client, `Bind ${style.cyan(entry.path)} to a workspace:`);
3719
+ const ws = await pickWorkspace(client, {
3720
+ promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
3721
+ dirName: basename3(entry.path)
3722
+ });
3539
3723
  if (!ws) {
3540
3724
  return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
3541
3725
  }
@@ -3578,7 +3762,7 @@ This fan-out will pin the same lane in every repo:
3578
3762
  async function runRecurse(cfg, g, opts) {
3579
3763
  const { yes, dryRun, json } = opts;
3580
3764
  const root = process.cwd();
3581
- const manifestPath = join7(root, ".sechroom", "repos.json");
3765
+ const manifestPath = join8(root, ".sechroom", "repos.json");
3582
3766
  const fromManifest = readManifest(manifestPath);
3583
3767
  const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
3584
3768
  const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
@@ -3762,14 +3946,21 @@ async function chooseWire(opts, yes) {
3762
3946
  return opts.mcp === false ? "agent-only" : "full";
3763
3947
  }
3764
3948
  var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
3949
+ function printNextStepBlock(heading, lines) {
3950
+ const rule = style.dim("\u2500".repeat(52));
3951
+ process.stdout.write(
3952
+ `
3953
+ ${rule}
3954
+ ${style.bold(heading)}
3955
+
3956
+ ` + lines.map((l) => ` ${l}`).join("\n") + `
3957
+ ${rule}
3958
+ `
3959
+ );
3960
+ }
3765
3961
  async function printStarterPrompt(mode, cfg) {
3766
3962
  if (mode === "cli") {
3767
- process.stdout.write(
3768
- `
3769
- ${style.bold("Next:")} pick up where you left off \u2014
3770
- ${style.cyan("sechroom continuity resume-me")}
3771
- `
3772
- );
3963
+ printNextStepBlock("Next \u2014 pick up where you left off:", [style.cyan("sechroom continuity resume-me")]);
3773
3964
  return;
3774
3965
  }
3775
3966
  let primary = FALLBACK_AGENT_PROMPT;
@@ -3781,21 +3972,16 @@ ${style.bold("Next:")} pick up where you left off \u2014
3781
3972
  } catch {
3782
3973
  }
3783
3974
  }
3784
- process.stdout.write(
3785
- `
3786
- ${style.bold("Next:")} paste this into your AI agent to get going \u2014
3787
- ${style.cyan(`"${primary}"`)}
3788
- `
3789
- );
3975
+ printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [style.cyan(`"${primary}"`)]);
3790
3976
  }
3791
3977
 
3792
3978
  // src/commands/sweep.ts
3793
- import { existsSync as existsSync8 } from "fs";
3794
- import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
3795
- var DEFAULT_MANIFEST = join8(".sechroom", "repos.json");
3979
+ import { existsSync as existsSync9 } from "fs";
3980
+ import { dirname as dirname6, join as join9, resolve as resolve2 } from "path";
3981
+ var DEFAULT_MANIFEST = join9(".sechroom", "repos.json");
3796
3982
  function planEntry(entry, root) {
3797
3983
  const dir = resolveChildDir(entry.path, root);
3798
- if (!existsSync8(dir)) {
3984
+ if (!existsSync9(dir)) {
3799
3985
  return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
3800
3986
  }
3801
3987
  if (committedBindingPath(dir)) {
@@ -3889,106 +4075,78 @@ Examples:
3889
4075
  }
3890
4076
 
3891
4077
  // src/commands/skills.ts
3892
- import { homedir as homedir6 } from "os";
3893
- import { join as join9 } from "path";
3894
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, rmSync as rmSync2, existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
3895
- var DEFAULT_SLUG = "operator-skills";
3896
- var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
3897
- var LOCK = ".sechroom-skills.json";
3898
- function skillsDir(global) {
3899
- return global ? join9(homedir6(), ".claude", "skills") : join9(process.cwd(), ".claude", "skills");
3900
- }
3901
- function tagValue2(tags, prefix) {
3902
- return (tags ?? []).find((t) => t.startsWith(prefix))?.slice(prefix.length);
3903
- }
3904
- function hasAny(tags, candidates) {
3905
- return (tags ?? []).some((t) => candidates.includes(t));
4078
+ import { join as join10 } from "path";
4079
+ import { existsSync as existsSync10, rmSync as rmSync2 } from "fs";
4080
+
4081
+ // src/commands/lane.ts
4082
+ var LANE_KEYS = ["code-lane", "design-lane"];
4083
+ function showLane(json) {
4084
+ const found = readSem();
4085
+ if (!found) {
4086
+ if (json) return emit({ path: null, values: {} }, true);
4087
+ return console.log(
4088
+ style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom lane set'.`)
4089
+ );
4090
+ }
4091
+ const resolved = { ...found.values };
4092
+ let suffixed = false;
4093
+ for (const k of LANE_KEYS) {
4094
+ const v = found.values[k];
4095
+ if (!v) continue;
4096
+ resolved[k] = applyWorktreeLaneSuffix(v);
4097
+ if (resolved[k] !== v) suffixed = true;
4098
+ }
4099
+ if (json) return emit({ path: found.path, values: resolved, worktreeSuffixApplied: suffixed }, true);
4100
+ console.log(style.dim(`from ${found.path}`));
4101
+ Object.entries(resolved).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
4102
+ if (suffixed) console.log(style.dim(" (worktree -N suffix applied \u2014 non-primary git worktree)"));
4103
+ }
4104
+ function setLane(opts) {
4105
+ if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
4106
+ const target = localSemPath();
4107
+ const values = readLocalSemValues();
4108
+ if (opts.codeLane) values["code-lane"] = opts.codeLane;
4109
+ if (opts.designLane) values["design-lane"] = opts.designLane;
4110
+ writeSem(values, target);
4111
+ if (opts.json) return emit({ path: target, values }, true);
4112
+ console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
4113
+ Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
4114
+ }
4115
+ function registerLane(program2) {
4116
+ const lane = program2.command("lane").description("Show this checkout's continuity lane pin (worktree-aware -N suffix applied)").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
4117
+ lane.command("set").description("Write this checkout's lane pin to ./.sechroom/lane.json").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
4118
+ (opts, cmd) => setLane({
4119
+ codeLane: opts.codeLane,
4120
+ designLane: opts.designLane,
4121
+ json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
4122
+ })
4123
+ );
4124
+ lane.addHelpText(
4125
+ "after",
4126
+ `
4127
+ Examples:
4128
+ $ sechroom lane show the resolved lane(s)
4129
+ $ sechroom lane set --code-lane claude-code-chris --design-lane claude-design-chris
4130
+
4131
+ In a non-primary git worktree the ratified concurrent-session -N suffix is auto-applied (SBC-1094).
4132
+ (Aliases: 'sechroom skills lane' / 'skills set-lane' \u2014 kept for back-compat.)`
4133
+ );
3906
4134
  }
4135
+
4136
+ // src/commands/skills.ts
3907
4137
  function registerSkills(program2) {
3908
- const skills = program2.command("skills").description("Install + manage operator skills from a bundle");
4138
+ const skills = program2.command("skills").description("Manage operator skills (materialised by `onboard`)");
3909
4139
  skills.addHelpText(
3910
4140
  "after",
3911
4141
  `
3912
4142
  Examples:
3913
- $ sechroom skills install --code-lane claude-code-chris --design-lane claude-design-chris
3914
- $ sechroom skills install operator-skills --surface claude-code --local
3915
4143
  $ sechroom skills list
3916
4144
  $ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
3917
4145
  $ sechroom skills lane
3918
- $ sechroom skills clean`
4146
+ $ sechroom skills clean
4147
+
4148
+ To install/refresh skills, run 'sechroom onboard' (it offers to materialise them).`
3919
4149
  );
3920
- skills.command("install [slug]").description(`Install a skills bundle (default ${DEFAULT_SLUG}) into your personal workspace + write SKILL.md files`).option("--version <v>", "bundle version (default: latest published in the catalogue)").option("--instance <name>", "install as a named, separate instance (install the same bundle more than once)").option("--code-lane <id>", "identity.code-lane binding (e.g. claude-code-chris)").option("--design-lane <id>", "identity.design-lane binding (e.g. claude-design-chris)").option("--surface <s>", "skill target surface to materialise", "claude-code").option("--local", "write to ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts, cmd) => {
3921
- const slug = slugArg || DEFAULT_SLUG;
3922
- const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
3923
- const pw = await runApi("resolving personal workspace", () => client.GET("/me/personal-workspace", {}));
3924
- const personalWsId = pw?.id || pw?.workspaceId || pw?.personalWorkspaceId || pw?.item?.id;
3925
- if (!personalWsId) fail("Could not resolve your personal workspace.");
3926
- let version = opts.version;
3927
- if (!version) {
3928
- const cat = await runApi("reading the bundle catalogue", () => client.GET("/me/bundles", {}));
3929
- const item = (cat?.bundles ?? cat?.Bundles ?? []).find((b) => (b.slug ?? b.Slug) === slug);
3930
- if (!item) fail(`Bundle '${slug}' is not in your self-serve catalogue (must be UserInstallable + Published).`);
3931
- version = item.latestVersion ?? item.LatestVersion;
3932
- if (!version) fail(`Bundle '${slug}' has no installable (Published) version.`);
3933
- }
3934
- const installOptions = {};
3935
- if (opts.codeLane) installOptions["identity.code-lane"] = opts.codeLane;
3936
- if (opts.designLane) installOptions["identity.design-lane"] = opts.designLane;
3937
- const res = await runApi(
3938
- `installing ${slug}@${version}${opts.instance ? ` (${opts.instance})` : ""}`,
3939
- () => client.POST("/me/bundles/{slug}/versions/{version}/install", {
3940
- params: { path: { slug, version } },
3941
- // instance: null/absent = the default instance (reinstall updates in
3942
- // place); a name installs a separate instance.
3943
- body: { installOptions, instance: opts.instance ?? null }
3944
- })
3945
- );
3946
- const status = String(res?.status ?? res?.Status ?? "");
3947
- if (status && status.toLowerCase() !== "completed") {
3948
- fail(`Install did not complete (status=${status}; ${res?.failureReason ?? res?.FailureReason ?? ""}).`);
3949
- }
3950
- const feed = await runApi(
3951
- "materialising skill files",
3952
- () => client.GET("/workspaces/{workspaceId}/memories/feed", {
3953
- // cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace of
3954
- // the personal workspace, so we recurse from the personal-ws root.
3955
- // includeText: the feed omits bodies by default; we need them for SKILL.md.
3956
- params: {
3957
- path: { workspaceId: personalWsId },
3958
- query: { limit: 200, cascadeWorkspaces: true, includeText: true }
3959
- }
3960
- })
3961
- );
3962
- const rows = feed?.results ?? feed?.Results ?? [];
3963
- const dir = skillsDir(!opts.local);
3964
- const wantInstance = opts.instance || "default";
3965
- const written = [];
3966
- const bundleTagPrefix = `sechroom:bundle:${slug}@`;
3967
- for (const r of rows) {
3968
- const m = r.item ?? r;
3969
- const tags = m.tags ?? m.Tags ?? [];
3970
- if (!hasAny(tags, ROLE_TAGS)) continue;
3971
- if (tagValue2(tags, "target:") !== opts.surface) continue;
3972
- if (!tags.some((t) => t.startsWith(bundleTagPrefix))) continue;
3973
- if ((tagValue2(tags, "sechroom:skill-instance:") ?? "default") !== wantInstance) continue;
3974
- const name = tagValue2(tags, "skill:");
3975
- if (!name) continue;
3976
- const body = m.text ?? m.Text ?? "";
3977
- mkdirSync6(join9(dir, name), { recursive: true });
3978
- writeFileSync6(join9(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
3979
- written.push(name);
3980
- }
3981
- mkdirSync6(dir, { recursive: true });
3982
- const lockPath = join9(dir, LOCK);
3983
- const lock = existsSync9(lockPath) ? JSON.parse(readFileSync6(lockPath, "utf8")) : {};
3984
- lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
3985
- writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
3986
- if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
3987
- const instanceNote = opts.instance ? ` (${opts.instance})` : "";
3988
- console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
3989
- written.forEach((n) => console.log(" " + style.dim("\u2022") + " " + n));
3990
- if (written.length === 0) console.log(style.dim(` (no '${opts.surface}' skill bodies found; check --surface)`));
3991
- });
3992
4150
  skills.command("list").description("List your installed bundles (GET /me/bundle-installs)").option("--json", "machine output").action(async (opts, cmd) => {
3993
4151
  const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
3994
4152
  const data = await runApi("reading your installs", () => client.GET("/me/bundle-installs", {}));
@@ -4001,49 +4159,33 @@ Examples:
4001
4159
  console.log(` ${i.bundleSlug ?? i.BundleSlug}@${i.bundleVersion ?? i.BundleVersion ?? "?"}${tag}`);
4002
4160
  });
4003
4161
  });
4004
- skills.command("clean [slug]").description(`Remove materialised skill files written by install (default ${DEFAULT_SLUG})`).option("--local", "clean ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts) => {
4005
- const slug = slugArg || DEFAULT_SLUG;
4162
+ skills.command("clean [slug]").description(`Remove skill files materialised by onboard (default ${DEFAULT_SKILLS_SLUG})`).option("--local", "clean ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts) => {
4163
+ const slug = slugArg || DEFAULT_SKILLS_SLUG;
4006
4164
  const dir = skillsDir(!opts.local);
4007
- const lockPath = join9(dir, LOCK);
4008
- if (!existsSync9(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
4009
- const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
4165
+ const lock = readSkillsLock(dir);
4010
4166
  const entry = lock[slug];
4011
- if (!entry) fail(`No installed record for '${slug}' in ${lockPath}.`);
4167
+ if (!entry) fail(`No materialised skills recorded for '${slug}' in ${join10(dir, SKILLS_LOCK)}.`);
4012
4168
  const removed = [];
4013
4169
  for (const name of entry.skills) {
4014
- const skillPath = join9(dir, name);
4015
- if (existsSync9(skillPath)) {
4170
+ const skillPath = join10(dir, name);
4171
+ if (existsSync10(skillPath)) {
4016
4172
  rmSync2(skillPath, { recursive: true, force: true });
4017
4173
  removed.push(name);
4018
4174
  }
4019
4175
  }
4020
4176
  delete lock[slug];
4021
- writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
4177
+ writeSkillsLock(dir, lock);
4022
4178
  if (opts.json) return emit({ slug, removed, dir }, true);
4023
4179
  console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
4024
4180
  });
4025
- skills.command("set-lane").description("Write this checkout's lane pin to a local ./.sechroom/lane.json file (read at runtime by skills)").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action((opts, cmd) => {
4026
- if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
4027
- const target = localSemPath();
4028
- const values = readLocalSemValues();
4029
- if (opts.codeLane) values["code-lane"] = opts.codeLane;
4030
- if (opts.designLane) values["design-lane"] = opts.designLane;
4031
- writeSem(values, target);
4032
- if (cmd.optsWithGlobals().json) return emit({ path: target, values }, true);
4033
- console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
4034
- Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
4035
- });
4036
- skills.command("lane").description("Show the lane pin resolved from ./.sechroom/lane.json (nearest in this checkout; legacy ./.sem honoured)").option("--json", "machine output").action((opts, cmd) => {
4037
- const json = cmd.optsWithGlobals().json;
4038
- const found = readSem();
4039
- if (!found) {
4040
- if (json) return emit({ path: null, values: {} }, true);
4041
- return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
4042
- }
4043
- if (json) return emit(found, true);
4044
- console.log(style.dim(`from ${found.path}`));
4045
- Object.entries(found.values).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
4046
- });
4181
+ skills.command("set-lane").description("Alias of `sechroom lane set` (kept for back-compat) \u2014 write this checkout's lane pin").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
4182
+ (opts, cmd) => setLane({
4183
+ codeLane: opts.codeLane,
4184
+ designLane: opts.designLane,
4185
+ json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
4186
+ })
4187
+ );
4188
+ skills.command("lane").description("Alias of `sechroom lane` (kept for back-compat) \u2014 show this checkout's lane pin").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
4047
4189
  skills.command("set-workflow").description("Set your per-operator workflow defaults (server-side; follows you across tenants)").option("--default-code-lane <id>", "personal default code lane (e.g. claude-code-chris)").option("--default-design-lane <id>", "personal default design lane (e.g. claude-design-chris)").option("--handover-recipient <id>", "your daily-handover counterparty (e.g. andy)").option("--json", "machine output").action(async (opts, cmd) => {
4048
4190
  if (!opts.defaultCodeLane && !opts.defaultDesignLane && !opts.handoverRecipient)
4049
4191
  fail("Provide at least one of --default-code-lane / --default-design-lane / --handover-recipient.");
@@ -4107,22 +4249,24 @@ Examples:
4107
4249
  }
4108
4250
 
4109
4251
  // src/commands/reset.ts
4110
- import { homedir as homedir7 } from "os";
4111
- import { join as join10 } from "path";
4112
- import { existsSync as existsSync10, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
4113
- var SKILLS_LOCK = ".sechroom-skills.json";
4114
- var localSkillsDir = () => join10(process.cwd(), ".claude", "skills");
4115
- var globalSkillsDir = () => join10(homedir7(), ".claude", "skills");
4252
+ import { homedir as homedir6 } from "os";
4253
+ import { join as join11 } from "path";
4254
+ import { existsSync as existsSync11, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
4255
+ var SKILLS_LOCK2 = ".sechroom-skills.json";
4256
+ var localSkillsDir = () => join11(process.cwd(), ".claude", "skills");
4257
+ var globalSkillsDir = () => join11(homedir6(), ".claude", "skills");
4258
+ var localAgentsDir = () => join11(process.cwd(), ".claude", "agents");
4259
+ var globalAgentsDir = () => join11(homedir6(), ".claude", "agents");
4116
4260
  function removeMaterialisedSkills(dir) {
4117
4261
  const removed = [];
4118
- const lockPath = join10(dir, SKILLS_LOCK);
4119
- if (!existsSync10(lockPath)) return removed;
4262
+ const lockPath = join11(dir, SKILLS_LOCK2);
4263
+ if (!existsSync11(lockPath)) return removed;
4120
4264
  try {
4121
4265
  const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
4122
4266
  for (const entry of Object.values(lock)) {
4123
4267
  for (const name of entry.skills ?? []) {
4124
- const p = join10(dir, name);
4125
- if (existsSync10(p)) {
4268
+ const p = join11(dir, name);
4269
+ if (existsSync11(p)) {
4126
4270
  rmSync3(p, { recursive: true, force: true });
4127
4271
  removed.push(p);
4128
4272
  }
@@ -4167,28 +4311,30 @@ function registerReset(program2) {
4167
4311
  }
4168
4312
  }
4169
4313
  const removed = [];
4170
- const stateDir = join10(process.cwd(), ".sechroom");
4171
- if (existsSync10(stateDir)) {
4314
+ const stateDir = join11(process.cwd(), ".sechroom");
4315
+ if (existsSync11(stateDir)) {
4172
4316
  rmSync3(stateDir, { recursive: true, force: true });
4173
4317
  removed.push(stateDir);
4174
4318
  }
4175
- const legacyCfg = join10(process.cwd(), ".sechroom.json");
4176
- if (existsSync10(legacyCfg)) {
4319
+ const legacyCfg = join11(process.cwd(), ".sechroom.json");
4320
+ if (existsSync11(legacyCfg)) {
4177
4321
  rmSync3(legacyCfg, { force: true });
4178
4322
  removed.push(legacyCfg);
4179
4323
  }
4180
- const legacySem = join10(process.cwd(), ".sem");
4181
- if (existsSync10(legacySem)) {
4324
+ const legacySem = join11(process.cwd(), ".sem");
4325
+ if (existsSync11(legacySem)) {
4182
4326
  rmSync3(legacySem, { force: true });
4183
4327
  removed.push(legacySem);
4184
4328
  }
4185
4329
  removed.push(...removeMaterialisedSkills(localSkillsDir()));
4330
+ removed.push(...removeMaterialisedSkills(localAgentsDir()));
4186
4331
  if (global) {
4187
4332
  const tok = clearToken();
4188
4333
  if (tok) removed.push(tok);
4189
4334
  const cfg = clearPersisted();
4190
4335
  if (cfg) removed.push(cfg);
4191
4336
  removed.push(...removeMaterialisedSkills(globalSkillsDir()));
4337
+ removed.push(...removeMaterialisedSkills(globalAgentsDir()));
4192
4338
  }
4193
4339
  if (json) return emit({ global, removed }, true);
4194
4340
  if (removed.length === 0) {
@@ -4326,6 +4472,7 @@ registerSetup(program);
4326
4472
  registerOnboard(program);
4327
4473
  registerSweep(program);
4328
4474
  registerSkills(program);
4475
+ registerLane(program);
4329
4476
  registerReset(program);
4330
4477
  program.parseAsync().catch((err2) => {
4331
4478
  process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sechroom/cli",
3
- "version": "2026.6.29",
3
+ "version": "2026.6.30",
4
4
  "description": "Sechroom CLI — a thin, generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",