@sechroom/cli 2026.6.14 → 2026.6.15-rc.5d5de589

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 +1346 -460
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync6 } from "fs";
4
+ import { readFileSync as readFileSync8 } from "fs";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/auth.ts
@@ -16,7 +16,10 @@ import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "fs";
16
16
  var CONFIG_DIR = join(homedir(), ".config", "sechroom");
17
17
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
18
18
  var TOKEN_FILE = join(CONFIG_DIR, "token.json");
19
- var LOCAL_CONFIG_NAME = ".sechroom.json";
19
+ var STATE_DIR_NAME = ".sechroom";
20
+ var BASELINE_CONFIG_NAME = ".sechroom.json";
21
+ var OVERRIDE_CONFIG_NAME = join(STATE_DIR_NAME, "config.json");
22
+ var BINDING_FIELDS = ["schemaVersion", "baseUrl", "tenant", "workspaceId", "defaultProjectId"];
20
23
  var DEFAULT_BASE_URL = "https://app.sechroom.ai/api";
21
24
  var LOCAL_CONFIG_SCHEMA_VERSION = 2;
22
25
  function ensureDir() {
@@ -64,43 +67,55 @@ function clearPersisted() {
64
67
  rmSync(CONFIG_FILE);
65
68
  return CONFIG_FILE;
66
69
  }
67
- function findLocalConfigPath(start = process.cwd()) {
70
+ function readJsonConfig(path) {
71
+ try {
72
+ return JSON.parse(readFileSync(path, "utf8"));
73
+ } catch {
74
+ return void 0;
75
+ }
76
+ }
77
+ function findConfigHome(start = process.cwd()) {
68
78
  let dir = start;
69
79
  for (; ; ) {
70
- const candidate = join(dir, LOCAL_CONFIG_NAME);
71
- if (existsSync(candidate)) return candidate;
80
+ if (existsSync(join(dir, BASELINE_CONFIG_NAME)) || existsSync(join(dir, OVERRIDE_CONFIG_NAME))) return dir;
72
81
  const parent = dirname(dir);
73
82
  if (parent === dir) return void 0;
74
83
  dir = parent;
75
84
  }
76
85
  }
77
86
  function readLocalConfig() {
78
- const path = findLocalConfigPath();
79
- if (!path) return {};
80
- try {
81
- const c = JSON.parse(readFileSync(path, "utf8"));
82
- return {
83
- schemaVersion: c.schemaVersion,
84
- baseUrl: c.baseUrl,
85
- tenant: c.tenant,
86
- workspaceId: c.workspaceId,
87
- defaultProjectId: c.defaultProjectId,
88
- path
89
- };
90
- } catch {
91
- return {};
92
- }
87
+ const home = findConfigHome();
88
+ if (!home) return {};
89
+ const baselinePath = join(home, BASELINE_CONFIG_NAME);
90
+ const overridePath = join(home, OVERRIDE_CONFIG_NAME);
91
+ const merged = { ...readJsonConfig(baselinePath) ?? {}, ...readJsonConfig(overridePath) ?? {} };
92
+ return {
93
+ schemaVersion: merged.schemaVersion,
94
+ baseUrl: merged.baseUrl,
95
+ tenant: merged.tenant,
96
+ workspaceId: merged.workspaceId,
97
+ defaultProjectId: merged.defaultProjectId,
98
+ path: existsSync(baselinePath) ? baselinePath : overridePath
99
+ };
93
100
  }
94
101
  function writeLocalConfig(patch) {
95
- const path = findLocalConfigPath() ?? join(process.cwd(), LOCAL_CONFIG_NAME);
96
- let current = {};
97
- try {
98
- current = JSON.parse(readFileSync(path, "utf8"));
99
- } catch {
100
- }
102
+ const home = findConfigHome() ?? process.cwd();
103
+ const baselinePath = join(home, BASELINE_CONFIG_NAME);
104
+ const overridePath = join(home, OVERRIDE_CONFIG_NAME);
105
+ const current = readJsonConfig(baselinePath) ?? {};
101
106
  const next = { ...current, ...patch, schemaVersion: LOCAL_CONFIG_SCHEMA_VERSION };
102
- writeFileSync(path, JSON.stringify(next, null, 2), { mode: 384 });
103
- return path;
107
+ writeFileSync(baselinePath, JSON.stringify(next, null, 2), { mode: 420 });
108
+ const override = readJsonConfig(overridePath);
109
+ if (override) {
110
+ for (const f of BINDING_FIELDS) delete override[f];
111
+ if (Object.keys(override).length === 0) rmSync(overridePath, { force: true });
112
+ else writeFileSync(overridePath, JSON.stringify(override, null, 2), { mode: 384 });
113
+ }
114
+ return baselinePath;
115
+ }
116
+ function committedBindingPath(dir) {
117
+ const p = join(dir, BASELINE_CONFIG_NAME);
118
+ return existsSync(p) ? p : void 0;
104
119
  }
105
120
  function resolveConfig(flags) {
106
121
  const local = readLocalConfig();
@@ -360,8 +375,8 @@ async function promptYesNo(question) {
360
375
  const { createInterface } = await import("readline");
361
376
  const rl = createInterface({ input: process.stdin, output: process.stderr });
362
377
  try {
363
- const answer = await new Promise((resolve) => {
364
- rl.question(`${question} [y/N] `, resolve);
378
+ const answer = await new Promise((resolve3) => {
379
+ rl.question(`${question} [y/N] `, resolve3);
365
380
  });
366
381
  return /^y(es)?$/i.test(answer.trim());
367
382
  } finally {
@@ -374,8 +389,8 @@ async function promptText(question, def) {
374
389
  const rl = createInterface({ input: process.stdin, output: process.stderr });
375
390
  try {
376
391
  const suffix = def ? ` [${def}]` : "";
377
- const answer = await new Promise((resolve) => {
378
- rl.question(`${question}${suffix} `, resolve);
392
+ const answer = await new Promise((resolve3) => {
393
+ rl.question(`${question}${suffix} `, resolve3);
379
394
  });
380
395
  const trimmed = answer.trim();
381
396
  return trimmed.length > 0 ? trimmed : def ?? "";
@@ -401,8 +416,8 @@ async function promptSelect(question, choices, def) {
401
416
  process.stderr.write(` ${marker} ${style.bold(String(i + 1))}. ${c.label}${hint}
402
417
  `);
403
418
  });
404
- const answer = await new Promise((resolve) => {
405
- rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve);
419
+ const answer = await new Promise((resolve3) => {
420
+ rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve3);
406
421
  });
407
422
  const trimmed = answer.trim();
408
423
  if (!trimmed) return choices[defIdx].value;
@@ -434,8 +449,8 @@ async function promptMultiSelect(question, choices, preselected = []) {
434
449
  process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
435
450
  `);
436
451
  });
437
- const answer = await new Promise((resolve) => {
438
- rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve);
452
+ const answer = await new Promise((resolve3) => {
453
+ rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve3);
439
454
  });
440
455
  const trimmed = answer.trim().toLowerCase();
441
456
  if (!trimmed) return preValues();
@@ -1558,115 +1573,738 @@ Examples:
1558
1573
  });
1559
1574
  }
1560
1575
 
1561
- // src/commands/account.ts
1562
- function registerId(program2) {
1563
- const id = program2.command("id").description("Allocate human-authored id sequences (FR-*, D-*)");
1564
- id.addHelpText(
1565
- "after",
1566
- `
1567
- Examples:
1568
- $ sechroom id next FR sechroom allocate the next FR-sechroom-NNN id
1569
- $ sechroom id next D Backend allocate the next D-Backend-NNN id
1570
- $ sechroom id peek FR sechroom inspect the sequence without consuming
1571
- $ sechroom id peek FR sechroom --json`
1572
- );
1573
- id.command("next <namespaceKind> <scope>").description("Allocate the next id in a sequence (POST /id-registry/allocate)").action(async (namespaceKind, scope, _opts, cmd) => {
1574
- const cfg = resolveConfig(cmd.optsWithGlobals());
1575
- const data = await runApi("Allocating id", async () => {
1576
- const client = await makeClient(cfg);
1577
- return client.POST("/id-registry/allocate", {
1578
- body: { namespaceKind, scope, clientNonce: null }
1579
- });
1580
- });
1581
- emitAction(`allocated ${style.bold(data.id)} ${style.dim(`(seq ${data.seq})`)}`, data, cmd.optsWithGlobals().json);
1582
- });
1583
- id.command("peek <namespaceKind> <scope>").description("Inspect a sequence without consuming (GET /id-registry/state)").action(async (namespaceKind, scope, _opts, cmd) => {
1584
- const cfg = resolveConfig(cmd.optsWithGlobals());
1585
- const data = await runApi("Peeking id sequence", async () => {
1586
- const client = await makeClient(cfg);
1587
- return client.GET("/id-registry/state", {
1588
- params: { query: { namespaceKind, scope } }
1589
- });
1590
- });
1591
- emit(data, cmd.optsWithGlobals().json);
1576
+ // src/commands/hook.ts
1577
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1578
+ import { homedir as homedir3 } from "os";
1579
+ import { delimiter, dirname as dirname4, join as join4 } from "path";
1580
+
1581
+ // src/sem.ts
1582
+ import { basename as basename2, dirname as dirname2, join as join2 } from "path";
1583
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1584
+ var SEM_FILE = join2(".sechroom", "lane.json");
1585
+ var LEGACY_SEM_FILE = ".sem";
1586
+ var STATE_DIR_NAME2 = ".sechroom";
1587
+ function localSemPath(cwd = process.cwd()) {
1588
+ return join2(cwd, SEM_FILE);
1589
+ }
1590
+ function resolveSemPathForRead(start = process.cwd()) {
1591
+ let dir = start;
1592
+ while (true) {
1593
+ const candidate = join2(dir, SEM_FILE);
1594
+ if (existsSync2(candidate)) return candidate;
1595
+ const legacy = join2(dir, LEGACY_SEM_FILE);
1596
+ if (existsSync2(legacy)) return legacy;
1597
+ const parent = dirname2(dir);
1598
+ if (parent === dir) return void 0;
1599
+ dir = parent;
1600
+ }
1601
+ }
1602
+ function parseSem(text) {
1603
+ const out = {};
1604
+ for (const raw of text.split("\n")) {
1605
+ const line = raw.trim();
1606
+ if (!line || line.startsWith("#")) continue;
1607
+ const eq = line.indexOf("=");
1608
+ if (eq === -1) continue;
1609
+ const key = line.slice(0, eq).trim();
1610
+ const value = line.slice(eq + 1).trim();
1611
+ if (key) out[key] = value;
1612
+ }
1613
+ return out;
1614
+ }
1615
+ function serializeSem(values) {
1616
+ return JSON.stringify(values, null, 2) + "\n";
1617
+ }
1618
+ function readSem(path) {
1619
+ const p = path ?? resolveSemPathForRead();
1620
+ if (!p || !existsSync2(p)) return void 0;
1621
+ const text = readFileSync2(p, "utf8");
1622
+ const values = basename2(p) === LEGACY_SEM_FILE ? parseSem(text) : parseLaneJson(text);
1623
+ return { path: p, values };
1624
+ }
1625
+ function readLocalSemValues(cwd = process.cwd()) {
1626
+ const next = join2(cwd, SEM_FILE);
1627
+ if (existsSync2(next)) return readSem(next)?.values ?? {};
1628
+ const legacy = join2(cwd, LEGACY_SEM_FILE);
1629
+ if (existsSync2(legacy)) return readSem(legacy)?.values ?? {};
1630
+ return {};
1631
+ }
1632
+ function parseLaneJson(text) {
1633
+ try {
1634
+ const parsed = JSON.parse(text);
1635
+ const out = {};
1636
+ for (const [k, v] of Object.entries(parsed)) {
1637
+ if (typeof v === "string") out[k] = v;
1638
+ }
1639
+ return out;
1640
+ } catch {
1641
+ return {};
1642
+ }
1643
+ }
1644
+ var STATE_DIR_IGNORE = `${STATE_DIR_NAME2}/`;
1645
+ function writeSem(values, path = localSemPath()) {
1646
+ mkdirSync2(dirname2(path), { recursive: true });
1647
+ writeFileSync2(path, serializeSem(values));
1648
+ ensureSemIgnored(path);
1649
+ return path;
1650
+ }
1651
+ function ignoresSem(content) {
1652
+ return content.split("\n").some((line) => {
1653
+ const t = line.trim();
1654
+ return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
1592
1655
  });
1593
1656
  }
1594
- function registerAccount(program2) {
1595
- const account = program2.command("account").description("Your profile, feeds, and review queue");
1596
- account.addHelpText(
1597
- "after",
1598
- `
1599
- Examples:
1600
- $ sechroom account profile
1601
- $ sechroom account set-profile --display-name "Chris" --timezone "Europe/London"
1602
- $ sechroom account feed --limit 20
1603
- $ sechroom account reviews --status Pending
1604
- $ sechroom account lookup-batch mem_XXXX wsp_YYYY --json`
1657
+ function inGitRepo(startDir) {
1658
+ let dir = startDir;
1659
+ for (; ; ) {
1660
+ if (existsSync2(join2(dir, ".git"))) return true;
1661
+ const parent = dirname2(dir);
1662
+ if (parent === dir) return false;
1663
+ dir = parent;
1664
+ }
1665
+ }
1666
+ function resolveGitignoreTarget(startDir) {
1667
+ let dir = startDir;
1668
+ for (; ; ) {
1669
+ const gi = join2(dir, ".gitignore");
1670
+ if (existsSync2(gi)) return { path: gi, exists: true };
1671
+ const parent = dirname2(dir);
1672
+ if (existsSync2(join2(dir, ".git")) || parent === dir) {
1673
+ return { path: join2(startDir, ".gitignore"), exists: false };
1674
+ }
1675
+ dir = parent;
1676
+ }
1677
+ }
1678
+ function ensureSemIgnored(semPath) {
1679
+ try {
1680
+ const checkoutDir = dirname2(dirname2(semPath));
1681
+ if (!inGitRepo(checkoutDir)) return;
1682
+ const target = resolveGitignoreTarget(checkoutDir);
1683
+ if (target.exists) {
1684
+ const content = readFileSync2(target.path, "utf8");
1685
+ if (ignoresSem(content)) return;
1686
+ const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
1687
+ appendFileSync(target.path, `${sep}${STATE_DIR_IGNORE}
1688
+ `);
1689
+ } else {
1690
+ writeFileSync2(target.path, `${STATE_DIR_IGNORE}
1691
+ `);
1692
+ }
1693
+ } catch {
1694
+ }
1695
+ }
1696
+
1697
+ // src/setup/clients.ts
1698
+ import { existsSync as existsSync3 } from "fs";
1699
+ import { homedir as homedir2 } from "os";
1700
+ import { dirname as dirname3, join as join3 } from "path";
1701
+
1702
+ // src/setup/operator-surface.ts
1703
+ var SectionType = {
1704
+ McpConfig: "mcp-config",
1705
+ McpConfigToml: "mcp-config-toml",
1706
+ InstructionFile: "instruction-file",
1707
+ ProjectConfig: "project-config",
1708
+ Verify: "verify",
1709
+ /** SBC-999 — workspace-pinned conventions, emitted only when the request
1710
+ * carried a workspaceId and that workspace has agent-setup-bundle memories. */
1711
+ WorkspaceConventions: "workspace-conventions"
1712
+ };
1713
+ async function fetchSetup(cfg) {
1714
+ const client = await makeClient(cfg);
1715
+ const { data, error } = await client.GET(
1716
+ "/operator-surface/setup",
1717
+ cfg.workspaceId ? { params: { query: { workspaceId: cfg.workspaceId } } } : {}
1605
1718
  );
1606
- account.command("profile").description("Show your resolved profile (GET /me/profile)").action(async (_opts, cmd) => {
1607
- const cfg = resolveConfig(cmd.optsWithGlobals());
1608
- const data = await runApi("Fetching profile", async () => {
1609
- const client = await makeClient(cfg);
1610
- return client.GET("/me/profile");
1611
- });
1612
- emit(data, cmd.optsWithGlobals().json);
1613
- });
1614
- account.command("set-profile").description("Update your profile (PUT /me/profile)").option("--display-name <displayName>", "Display name").option("--timezone <timezone>", "IANA timezone (e.g. Europe/London)").option("--bio <bio>", "Short bio").option("--photo-url <photoUrl>", "Avatar / photo URL").action(async (opts, cmd) => {
1615
- const cfg = resolveConfig(cmd.optsWithGlobals());
1616
- const data = await runApi("Updating profile", async () => {
1617
- const client = await makeClient(cfg);
1618
- return client.PUT("/me/profile", {
1619
- body: {
1620
- displayName: opts.displayName ?? null,
1621
- timezone: opts.timezone ?? null,
1622
- bio: opts.bio ?? null,
1623
- photoUrl: opts.photoUrl ?? null
1624
- }
1625
- });
1719
+ if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
1720
+ return data;
1721
+ }
1722
+ function findSurface(setup, surfaceKey) {
1723
+ return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
1724
+ }
1725
+ function findSection(surface, sectionType) {
1726
+ return surface?.sections.find((s) => s.sectionType === sectionType);
1727
+ }
1728
+ function sectionSnippet(section) {
1729
+ if (!section) return null;
1730
+ for (const step of section.steps) {
1731
+ if (step.copyValue) return step.copyValue;
1732
+ if (step.codeSnippet) return step.codeSnippet;
1733
+ }
1734
+ return null;
1735
+ }
1736
+ function parseTagArtifactId(id) {
1737
+ if (!id.startsWith("tag:")) return null;
1738
+ const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
1739
+ return tags.length > 0 ? tags : null;
1740
+ }
1741
+ async function getPersonalWorkspaceId(cfg) {
1742
+ const client = await makeClient(cfg);
1743
+ const { data } = await client.GET("/me/personal-workspace", {});
1744
+ return data?.workspaceId ?? null;
1745
+ }
1746
+ async function fetchMemoryFields(cfg, id) {
1747
+ const client = await makeClient(cfg);
1748
+ const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
1749
+ const env = data;
1750
+ const m = env?.item ?? env;
1751
+ if (!m) return null;
1752
+ const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
1753
+ return { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
1754
+ }
1755
+ async function resolveInstruction(cfg, section, personalWorkspaceId) {
1756
+ const client = await makeClient(cfg);
1757
+ for (const artifact of section.artifacts) {
1758
+ const tags = parseTagArtifactId(artifact.id);
1759
+ if (!tags) continue;
1760
+ const { data } = await client.POST("/memories/search", {
1761
+ body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags }
1626
1762
  });
1627
- emitAction("updated profile", data, cmd.optsWithGlobals().json);
1628
- });
1629
- account.command("feed").description("Your recent memory feed (GET /me/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Opaque paging cursor").option("--query <query>", "Free-text filter").option("--filter-tags <tags>", "Comma-separated tag filter").option("--include-archived", "Include archived memories", false).option("--include-text", "Include memory body text", false).action(async (opts, cmd) => {
1630
- const cfg = resolveConfig(cmd.optsWithGlobals());
1631
- const data = await runApi("Fetching feed", async () => {
1632
- const client = await makeClient(cfg);
1633
- return client.GET("/me/memories/feed", {
1634
- params: {
1635
- query: {
1636
- limit: Number(opts.limit),
1637
- includeArchived: Boolean(opts.includeArchived),
1638
- includeText: Boolean(opts.includeText),
1639
- ...opts.cursor ? { cursor: opts.cursor } : {},
1640
- ...opts.query ? { query: opts.query } : {},
1641
- ...opts.filterTags ? { filterTags: opts.filterTags } : {}
1642
- }
1643
- }
1763
+ const hits = data ?? [];
1764
+ if (hits.length === 0) continue;
1765
+ const templateId = hits[0].id;
1766
+ const template = await fetchMemoryFields(cfg, templateId);
1767
+ if (typeof template?.text !== "string" || template.text.length === 0) continue;
1768
+ const templateTags = template.tags ?? tags;
1769
+ if (personalWorkspaceId) {
1770
+ const { data: ovr } = await client.POST("/memories/search", {
1771
+ body: { query: null, textQuery: null, semanticQuery: null, hybrid: false, limit: 1, includeArchived: false, includeSystem: false, tags: ["sechroom:role:override", `sechroom:template-ref:${templateId}`], owner: { type: "Workspace", id: personalWorkspaceId } }
1644
1772
  });
1645
- });
1646
- emit(data, cmd.optsWithGlobals().json);
1647
- });
1648
- account.command("reviews").description("Your review queue (GET /reviews)").option("--status <status>", "Pending | Resolved | Empty").option("--scope-kind <scopeKind>", "Memory | Project | Workspace").option("--scope-target-id <id>", "Scope target id").option("--limit <n>", "Max results", "20").action(async (opts, cmd) => {
1649
- const cfg = resolveConfig(cmd.optsWithGlobals());
1650
- const data = await runApi("Fetching reviews", async () => {
1651
- const client = await makeClient(cfg);
1652
- return client.GET("/reviews", {
1653
- params: {
1654
- query: {
1655
- limit: Number(opts.limit),
1656
- ...opts.status ? { status: opts.status } : {},
1657
- ...opts.scopeKind ? { scopeKind: opts.scopeKind } : {},
1658
- ...opts.scopeTargetId ? { scopeTargetId: opts.scopeTargetId } : {}
1659
- }
1773
+ const ovrHits = ovr ?? [];
1774
+ if (ovrHits.length > 0) {
1775
+ const override = await fetchMemoryFields(cfg, ovrHits[0].id);
1776
+ if (typeof override?.text === "string" && override.text.length > 0) {
1777
+ return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
1660
1778
  }
1661
- });
1662
- });
1663
- emit(data, cmd.optsWithGlobals().json);
1664
- });
1665
- account.command("review-get <reviewId>").description("Fetch one review bundle (GET /reviews/{reviewId})").action(async (reviewId, _opts, cmd) => {
1666
- const cfg = resolveConfig(cmd.optsWithGlobals());
1667
- const data = await runApi("Fetching review", async () => {
1668
- const client = await makeClient(cfg);
1669
- return client.GET("/reviews/{reviewId}", { params: { path: { reviewId } } });
1779
+ }
1780
+ }
1781
+ return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
1782
+ }
1783
+ return null;
1784
+ }
1785
+ async function resolveWorkspaceConventions(cfg, section) {
1786
+ const parts = [];
1787
+ const refs = [];
1788
+ for (const artifact of section.artifacts) {
1789
+ if (parseTagArtifactId(artifact.id)) continue;
1790
+ const mem = await fetchMemoryFields(cfg, artifact.id);
1791
+ if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
1792
+ parts.push(mem.text.trim());
1793
+ refs.push(`${artifact.id}@v${mem.version ?? 1}`);
1794
+ }
1795
+ }
1796
+ if (parts.length === 0) return null;
1797
+ return { body: parts.join("\n\n---\n\n"), refs };
1798
+ }
1799
+ async function createOverride(cfg, template, personalWorkspaceId) {
1800
+ const client = await makeClient(cfg);
1801
+ const overrideTags = template.templateTags.filter(
1802
+ (t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
1803
+ );
1804
+ overrideTags.push("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
1805
+ const { error } = await client.POST("/memories", {
1806
+ body: {
1807
+ text: template.body,
1808
+ type: "reference",
1809
+ content: "{}",
1810
+ confidence: 1,
1811
+ source: "cli-agent-instructions-customize",
1812
+ archetype: "Document",
1813
+ title: template.title ?? null,
1814
+ tags: overrideTags,
1815
+ owner: { type: "Workspace", id: personalWorkspaceId }
1816
+ }
1817
+ });
1818
+ if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
1819
+ }
1820
+
1821
+ // src/setup/clients.ts
1822
+ function claudeDesktopConfigPath(home) {
1823
+ switch (process.platform) {
1824
+ case "darwin":
1825
+ return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1826
+ case "win32":
1827
+ return join3(process.env.APPDATA ?? join3(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
1828
+ default:
1829
+ return join3(home, ".config", "Claude", "claude_desktop_config.json");
1830
+ }
1831
+ }
1832
+ function clientTargets(cwd) {
1833
+ const home = homedir2();
1834
+ return {
1835
+ "claude-code": {
1836
+ key: "claude-code",
1837
+ label: "Claude Code",
1838
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".mcp.json"), format: "json" },
1839
+ instruction: { surfaceKey: "claude-code", path: join3(cwd, "CLAUDE.md") }
1840
+ },
1841
+ "claude-desktop": {
1842
+ key: "claude-desktop",
1843
+ label: "Claude Desktop",
1844
+ mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
1845
+ instruction: { surfaceKey: "claude-desktop", path: join3(home, ".claude", "CLAUDE.md") }
1846
+ },
1847
+ codex: {
1848
+ key: "codex",
1849
+ label: "Codex CLI",
1850
+ mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join3(home, ".codex", "config.toml"), format: "toml" },
1851
+ instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
1852
+ },
1853
+ cursor: {
1854
+ key: "cursor",
1855
+ label: "Cursor",
1856
+ mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join3(cwd, ".cursor", "mcp.json"), format: "json" },
1857
+ instruction: { surfaceKey: "chatgpt", path: join3(cwd, "AGENTS.md") }
1858
+ }
1859
+ };
1860
+ }
1861
+ var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
1862
+ var DEFAULT_CLIENT_KEY = "claude-code";
1863
+ function detectInstalledClients(cwd) {
1864
+ const home = homedir2();
1865
+ const detected = [];
1866
+ if (existsSync3(join3(home, ".claude"))) detected.push("claude-code");
1867
+ if (existsSync3(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
1868
+ if (existsSync3(join3(home, ".codex"))) detected.push("codex");
1869
+ if (existsSync3(join3(home, ".cursor")) || existsSync3(join3(cwd, ".cursor"))) detected.push("cursor");
1870
+ return detected;
1871
+ }
1872
+
1873
+ // src/commands/hook.ts
1874
+ async function readStdin() {
1875
+ if (process.stdin.isTTY) return "";
1876
+ const chunks = [];
1877
+ for await (const chunk of process.stdin) chunks.push(chunk);
1878
+ return Buffer.concat(chunks).toString("utf8");
1879
+ }
1880
+ function parseHookInput(raw) {
1881
+ if (!raw.trim()) return {};
1882
+ try {
1883
+ return JSON.parse(raw);
1884
+ } catch {
1885
+ return {};
1886
+ }
1887
+ }
1888
+ function resolveLane(flagLane, cwd) {
1889
+ if (flagLane) return flagLane;
1890
+ const env = process.env.SECHROOM_LANE;
1891
+ if (env) return env;
1892
+ const start = cwd ?? process.cwd();
1893
+ const sem = readSem(resolveSemPathForRead(start));
1894
+ return sem?.values["code-lane"];
1895
+ }
1896
+ var INTENT_FILE = join4(".sechroom", "continuity.json");
1897
+ function resolveIntentPath(start) {
1898
+ let dir = start;
1899
+ for (; ; ) {
1900
+ const candidate = join4(dir, INTENT_FILE);
1901
+ if (existsSync4(candidate)) return candidate;
1902
+ const parent = dirname4(dir);
1903
+ if (parent === dir) return void 0;
1904
+ dir = parent;
1905
+ }
1906
+ }
1907
+ function readIntent(start) {
1908
+ const path = resolveIntentPath(start);
1909
+ if (!path) return void 0;
1910
+ try {
1911
+ return JSON.parse(readFileSync3(path, "utf8"));
1912
+ } catch {
1913
+ return void 0;
1914
+ }
1915
+ }
1916
+ function hasRequiredIntent(i) {
1917
+ return Boolean(
1918
+ i.objective?.trim() && i.state?.trim() && i.lastAction?.trim() && i.nextAction?.trim() && i.resumeInstruction?.trim()
1919
+ );
1920
+ }
1921
+ function formatContext(bundle, lane) {
1922
+ const s = bundle?.latestSnapshot;
1923
+ if (!s) return null;
1924
+ const lines = [];
1925
+ lines.push(`[sechroom continuity \u2014 resumed lane ${lane}]`);
1926
+ if (s.currentObjective) lines.push(`Objective: ${s.currentObjective}`);
1927
+ if (s.currentState) lines.push(`State: ${s.currentState}`);
1928
+ if (s.lastMeaningfulAction) lines.push(`Last action: ${s.lastMeaningfulAction}`);
1929
+ if (s.nextIntendedAction) lines.push(`Next: ${s.nextIntendedAction}`);
1930
+ if (s.resumeInstruction) lines.push(`Resume: ${s.resumeInstruction}`);
1931
+ const constraints = s.activeConstraints ?? [];
1932
+ if (constraints.length) {
1933
+ lines.push("Active constraints:");
1934
+ for (const c of constraints) lines.push(` - ${c}`);
1935
+ }
1936
+ const questions = s.openQuestions ?? [];
1937
+ if (questions.length) {
1938
+ lines.push("Open questions:");
1939
+ for (const q of questions) lines.push(` - ${q}`);
1940
+ }
1941
+ const artifacts = s.relevantArtifactIds ?? [];
1942
+ if (artifacts.length) lines.push(`Relevant artifacts: ${artifacts.join(", ")}`);
1943
+ const marker = [s.id, s.createdAt].filter(Boolean).join(" @ ");
1944
+ if (marker) lines.push(`(snapshot ${marker})`);
1945
+ return lines.join("\n");
1946
+ }
1947
+ function emitSessionStart(additionalContext) {
1948
+ process.stdout.write(
1949
+ JSON.stringify({
1950
+ hookSpecificOutput: {
1951
+ hookEventName: "SessionStart",
1952
+ additionalContext
1953
+ }
1954
+ }) + "\n"
1955
+ );
1956
+ }
1957
+ var HOOK_COMMANDS = {
1958
+ SessionStart: "sechroom hook session-start",
1959
+ PreCompact: "sechroom hook pre-compact"
1960
+ };
1961
+ var HOOK_EVENTS = ["SessionStart", "PreCompact"];
1962
+ function hasHookCommand(config2, event, command) {
1963
+ const groups = config2.hooks?.[event] ?? [];
1964
+ return groups.some((g) => (g.hooks ?? []).some((h) => h.type === "command" && h.command === command));
1965
+ }
1966
+ function mergeHooks(config2) {
1967
+ config2.hooks ??= {};
1968
+ let added = 0;
1969
+ for (const event of HOOK_EVENTS) {
1970
+ const command = HOOK_COMMANDS[event];
1971
+ if (hasHookCommand(config2, event, command)) continue;
1972
+ const groups = config2.hooks[event] ??= [];
1973
+ groups.push({ hooks: [{ type: "command", command }] });
1974
+ added += 1;
1975
+ }
1976
+ return added;
1977
+ }
1978
+ function readJsonConfig2(path) {
1979
+ if (!existsSync4(path)) return {};
1980
+ const raw = readFileSync3(path, "utf8");
1981
+ if (!raw.trim()) return {};
1982
+ return JSON.parse(raw);
1983
+ }
1984
+ function installHooksJson(path, dryRun) {
1985
+ const existed = existsSync4(path) && readFileSync3(path, "utf8").trim().length > 0;
1986
+ const config2 = readJsonConfig2(path);
1987
+ const added = mergeHooks(config2);
1988
+ if (added === 0 && existed) return { path, status: "current" };
1989
+ if (!dryRun) {
1990
+ mkdirSync3(dirname4(path), { recursive: true });
1991
+ writeFileSync3(path, JSON.stringify(config2, null, 2) + "\n");
1992
+ }
1993
+ return { path, status: existed ? "merged" : "created" };
1994
+ }
1995
+ function ensureCodexFeaturesHooks(content) {
1996
+ const lines = content.split("\n");
1997
+ const headerIdx = lines.findIndex((l) => l.trim() === "[features]");
1998
+ if (headerIdx === -1) {
1999
+ const base = content.length === 0 || content.endsWith("\n") ? content : content + "\n";
2000
+ return { next: base + "\n[features]\nhooks = true\n", changed: true };
2001
+ }
2002
+ for (let i = headerIdx + 1; i < lines.length; i += 1) {
2003
+ const trimmed = lines[i].trim();
2004
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) break;
2005
+ const m = lines[i].match(/^(\s*)hooks(\s*)=(\s*)(.*)$/);
2006
+ if (!m) continue;
2007
+ const value = m[4].replace(/\s*#.*$/, "").trim();
2008
+ if (value === "true") return { next: content, changed: false };
2009
+ lines[i] = `${m[1]}hooks${m[2]}=${m[3]}true`;
2010
+ return { next: lines.join("\n"), changed: true };
2011
+ }
2012
+ lines.splice(headerIdx + 1, 0, "hooks = true");
2013
+ return { next: lines.join("\n"), changed: true };
2014
+ }
2015
+ function installCodexFeatureFlag(path, dryRun) {
2016
+ const existed = existsSync4(path);
2017
+ const content = existed ? readFileSync3(path, "utf8") : "";
2018
+ const { next, changed } = ensureCodexFeaturesHooks(content);
2019
+ if (!changed) return { path, status: "current" };
2020
+ if (!dryRun) {
2021
+ mkdirSync3(dirname4(path), { recursive: true });
2022
+ writeFileSync3(path, next);
2023
+ }
2024
+ return { path, status: existed ? "merged" : "created" };
2025
+ }
2026
+ function resolveSurfaces(surface, cwd) {
2027
+ if (surface === "claude") return ["claude"];
2028
+ if (surface === "codex") return ["codex"];
2029
+ if (surface === "both") return ["claude", "codex"];
2030
+ if (surface) throw new Error(`--surface must be one of claude | codex | both (got '${surface}')`);
2031
+ const surfaces = detectHookSurfaces(cwd);
2032
+ return surfaces.length > 0 ? surfaces : ["claude", "codex"];
2033
+ }
2034
+ function describe(result, dryRun) {
2035
+ if (result.status === "current") return ` \u2713 ${result.path} (already configured)`;
2036
+ const verb = dryRun ? "would" : result.status === "created" ? "created" : "updated";
2037
+ return ` \u2713 ${result.path} (${dryRun ? `${verb} ${result.status === "created" ? "create" : "update"}` : verb})`;
2038
+ }
2039
+ var HOOK_SURFACE_LABEL = {
2040
+ claude: "Claude Code",
2041
+ codex: "Codex"
2042
+ };
2043
+ function installHookSurfaces(surfaces, opts) {
2044
+ const out = [];
2045
+ for (const surface of surfaces) {
2046
+ if (surface === "claude") {
2047
+ const path = opts.local ? join4(opts.cwd, ".claude", "settings.json") : join4(opts.home, ".claude", "settings.json");
2048
+ out.push({ surface, results: [installHooksJson(path, opts.dryRun)] });
2049
+ } else {
2050
+ const hooksJson = installHooksJson(join4(opts.home, ".codex", "hooks.json"), opts.dryRun);
2051
+ const featureFlag = installCodexFeatureFlag(join4(opts.home, ".codex", "config.toml"), opts.dryRun);
2052
+ out.push({ surface, results: [hooksJson, featureFlag] });
2053
+ }
2054
+ }
2055
+ return out;
2056
+ }
2057
+ function detectHookSurfaces(cwd) {
2058
+ const detected = detectInstalledClients(cwd);
2059
+ const surfaces = [];
2060
+ if (detected.includes("claude-code")) surfaces.push("claude");
2061
+ if (detected.includes("codex")) surfaces.push("codex");
2062
+ return surfaces;
2063
+ }
2064
+ function isSechroomOnPath() {
2065
+ const pathEnv = process.env.PATH ?? "";
2066
+ if (!pathEnv) return false;
2067
+ const names = process.platform === "win32" ? ["sechroom.cmd", "sechroom.exe", "sechroom.bat", "sechroom"] : ["sechroom"];
2068
+ for (const dir of pathEnv.split(delimiter)) {
2069
+ if (!dir) continue;
2070
+ for (const name of names) {
2071
+ if (existsSync4(join4(dir, name))) return true;
2072
+ }
2073
+ }
2074
+ return false;
2075
+ }
2076
+ function warnIfSechroomNotOnPath(write = (s) => void process.stderr.write(s)) {
2077
+ if (isSechroomOnPath()) return false;
2078
+ write(
2079
+ "\n\u26A0 `sechroom` isn't on your PATH. The hooks run a bare `sechroom hook \u2026` command\n when your agent fires them, so a non-global install (npx / local) will fail at\n that point. Install globally so the command resolves:\n npm i -g @sechroom/cli\n"
2080
+ );
2081
+ return true;
2082
+ }
2083
+ function registerHook(program2) {
2084
+ const hook = program2.command("hook").description("Agent-lifecycle hook adapter (Claude Code / Codex) \u2014 bridges hooks to continuity");
2085
+ hook.addHelpText(
2086
+ "after",
2087
+ `
2088
+ Examples:
2089
+ # SessionStart (load): inject the lane's latest snapshot as context.
2090
+ $ echo '{"hook_event_name":"SessionStart","cwd":"'"$PWD"'"}' | sechroom hook session-start
2091
+ # PreCompact (save): snapshot from ./${INTENT_FILE} before the agent compacts.
2092
+ $ echo '{"hook_event_name":"PreCompact","cwd":"'"$PWD"'"}' | sechroom hook pre-compact
2093
+ # Wire both hooks into the installed surface(s)' config (no hand-editing):
2094
+ $ sechroom hook install auto-detect Claude Code / Codex
2095
+ $ sechroom hook install --surface codex Codex only
2096
+ $ sechroom hook install --local --dry-run preview the project .claude/settings.json
2097
+
2098
+ Lane source (high -> low): --lane > SECHROOM_LANE > ./.sem code-lane (D-binding-5).
2099
+ Fail-soft: no lane / no auth / no-or-partial intent file / API error -> exit 0, never blocks.`
2100
+ );
2101
+ hook.command("session-start").description("Resume the checkout's lane and emit continuity context for a SessionStart hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sem code-lane)").option("--surface <surface>", "Target surface: claude | codex (output is identical for session-start)", "claude").option("--max-artifacts <n>", "Cap artifacts in the resume bundle").action(async (opts, cmd) => {
2102
+ try {
2103
+ const raw = await readStdin();
2104
+ const input = parseHookInput(raw);
2105
+ const lane = resolveLane(opts.lane, input.cwd);
2106
+ if (!lane) return process.exit(0);
2107
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2108
+ const client = await makeClient(cfg);
2109
+ const { data } = await client.POST("/continuity/resume/lane", {
2110
+ body: {
2111
+ laneId: lane,
2112
+ workspaceId: null,
2113
+ maxArtifacts: opts.maxArtifacts != null ? Number(opts.maxArtifacts) : null,
2114
+ includeLookingAtMyself: null,
2115
+ changedSince: null
2116
+ }
2117
+ });
2118
+ const context = formatContext(data, lane);
2119
+ if (context) emitSessionStart(context);
2120
+ return process.exit(0);
2121
+ } catch {
2122
+ return process.exit(0);
2123
+ }
2124
+ });
2125
+ hook.command("pre-compact").description("Save a continuity snapshot from the agent-maintained intent file on a PreCompact hook").option("--lane <laneId>", "Override the resolved lane (else SECHROOM_LANE, else ./.sem code-lane)").option("--scope <scope>", "Snapshot scope (else the intent file's `scope`, else 'compaction')").option("--surface <surface>", "Target surface: claude | codex (lifecycle-only on both)", "claude").action(async (opts, cmd) => {
2126
+ try {
2127
+ const raw = await readStdin();
2128
+ const input = parseHookInput(raw);
2129
+ const cwd = input.cwd ?? process.cwd();
2130
+ const lane = resolveLane(opts.lane, input.cwd);
2131
+ if (!lane) return process.exit(0);
2132
+ const intent = readIntent(cwd);
2133
+ if (!intent || !hasRequiredIntent(intent)) return process.exit(0);
2134
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2135
+ const client = await makeClient(cfg);
2136
+ await client.POST("/continuity/snapshots", {
2137
+ body: {
2138
+ laneId: lane,
2139
+ scope: opts.scope ?? intent.scope ?? "compaction",
2140
+ currentObjective: intent.objective,
2141
+ currentState: intent.state,
2142
+ lastMeaningfulAction: intent.lastAction,
2143
+ nextIntendedAction: intent.nextAction,
2144
+ resumeInstruction: intent.resumeInstruction,
2145
+ activeConstraints: intent.constraints ?? null,
2146
+ openQuestions: intent.questions ?? null,
2147
+ surfaceMarkers: intent.surfaceMarkers ?? null,
2148
+ relevantArtifactIds: intent.artifacts ?? null,
2149
+ confidence: intent.confidence ?? null,
2150
+ // Compaction is infrequent, so the FR-051 clobber guard doesn't bite;
2151
+ // Acknowledge lets a within-window checkpoint land on the lane.
2152
+ concurrentSessionPolicy: "Acknowledge"
2153
+ }
2154
+ });
2155
+ return process.exit(0);
2156
+ } catch {
2157
+ return process.exit(0);
2158
+ }
2159
+ });
2160
+ hook.command("install").description("Wire the session-start + pre-compact hooks into Claude Code and/or Codex config").option("--surface <surface>", "Target surface: claude | codex | both (default: auto-detect installed surfaces)").option("--local", "Claude Code only: write <cwd>/.claude/settings.json instead of ~/.claude/settings.json").option("--dry-run", "Print what would change; write nothing").action((opts) => {
2161
+ const dryRun = Boolean(opts.dryRun);
2162
+ const cwd = process.cwd();
2163
+ let surfaces;
2164
+ try {
2165
+ surfaces = resolveSurfaces(opts.surface, cwd);
2166
+ } catch (err2) {
2167
+ process.stderr.write(`${err2.message}
2168
+ `);
2169
+ return process.exit(2);
2170
+ }
2171
+ const results = [];
2172
+ try {
2173
+ const installed = installHookSurfaces(surfaces, { dryRun, local: opts.local, cwd, home: homedir3() });
2174
+ for (const { surface, results: surfaceResults } of installed) {
2175
+ process.stdout.write(`${HOOK_SURFACE_LABEL[surface]}:
2176
+ `);
2177
+ for (const r of surfaceResults) {
2178
+ results.push(r);
2179
+ process.stdout.write(describe(r, dryRun) + "\n");
2180
+ }
2181
+ }
2182
+ } catch (err2) {
2183
+ process.stderr.write(`hook install failed: ${err2.message}
2184
+ `);
2185
+ return process.exit(1);
2186
+ }
2187
+ if (dryRun) {
2188
+ process.stdout.write("\n(dry run \u2014 no files were written.)\n");
2189
+ } else if (results.every((r) => r.status === "current")) {
2190
+ process.stdout.write("\nAlready up to date \u2014 nothing to change.\n");
2191
+ } else {
2192
+ process.stdout.write("\nRestart (or reload) your agent for the hooks to take effect.\n");
2193
+ }
2194
+ warnIfSechroomNotOnPath();
2195
+ return process.exit(0);
2196
+ });
2197
+ }
2198
+
2199
+ // src/commands/account.ts
2200
+ function registerId(program2) {
2201
+ const id = program2.command("id").description("Allocate human-authored id sequences (FR-*, D-*)");
2202
+ id.addHelpText(
2203
+ "after",
2204
+ `
2205
+ Examples:
2206
+ $ sechroom id next FR sechroom allocate the next FR-sechroom-NNN id
2207
+ $ sechroom id next D Backend allocate the next D-Backend-NNN id
2208
+ $ sechroom id peek FR sechroom inspect the sequence without consuming
2209
+ $ sechroom id peek FR sechroom --json`
2210
+ );
2211
+ id.command("next <namespaceKind> <scope>").description("Allocate the next id in a sequence (POST /id-registry/allocate)").action(async (namespaceKind, scope, _opts, cmd) => {
2212
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2213
+ const data = await runApi("Allocating id", async () => {
2214
+ const client = await makeClient(cfg);
2215
+ return client.POST("/id-registry/allocate", {
2216
+ body: { namespaceKind, scope, clientNonce: null }
2217
+ });
2218
+ });
2219
+ emitAction(`allocated ${style.bold(data.id)} ${style.dim(`(seq ${data.seq})`)}`, data, cmd.optsWithGlobals().json);
2220
+ });
2221
+ id.command("peek <namespaceKind> <scope>").description("Inspect a sequence without consuming (GET /id-registry/state)").action(async (namespaceKind, scope, _opts, cmd) => {
2222
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2223
+ const data = await runApi("Peeking id sequence", async () => {
2224
+ const client = await makeClient(cfg);
2225
+ return client.GET("/id-registry/state", {
2226
+ params: { query: { namespaceKind, scope } }
2227
+ });
2228
+ });
2229
+ emit(data, cmd.optsWithGlobals().json);
2230
+ });
2231
+ }
2232
+ function registerAccount(program2) {
2233
+ const account = program2.command("account").description("Your profile, feeds, and review queue");
2234
+ account.addHelpText(
2235
+ "after",
2236
+ `
2237
+ Examples:
2238
+ $ sechroom account profile
2239
+ $ sechroom account set-profile --display-name "Chris" --timezone "Europe/London"
2240
+ $ sechroom account feed --limit 20
2241
+ $ sechroom account reviews --status Pending
2242
+ $ sechroom account lookup-batch mem_XXXX wsp_YYYY --json`
2243
+ );
2244
+ account.command("profile").description("Show your resolved profile (GET /me/profile)").action(async (_opts, cmd) => {
2245
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2246
+ const data = await runApi("Fetching profile", async () => {
2247
+ const client = await makeClient(cfg);
2248
+ return client.GET("/me/profile");
2249
+ });
2250
+ emit(data, cmd.optsWithGlobals().json);
2251
+ });
2252
+ account.command("set-profile").description("Update your profile (PUT /me/profile)").option("--display-name <displayName>", "Display name").option("--timezone <timezone>", "IANA timezone (e.g. Europe/London)").option("--bio <bio>", "Short bio").option("--photo-url <photoUrl>", "Avatar / photo URL").action(async (opts, cmd) => {
2253
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2254
+ const data = await runApi("Updating profile", async () => {
2255
+ const client = await makeClient(cfg);
2256
+ return client.PUT("/me/profile", {
2257
+ body: {
2258
+ displayName: opts.displayName ?? null,
2259
+ timezone: opts.timezone ?? null,
2260
+ bio: opts.bio ?? null,
2261
+ photoUrl: opts.photoUrl ?? null
2262
+ }
2263
+ });
2264
+ });
2265
+ emitAction("updated profile", data, cmd.optsWithGlobals().json);
2266
+ });
2267
+ account.command("feed").description("Your recent memory feed (GET /me/memories/feed)").option("--limit <n>", "Max results", "20").option("--cursor <cursor>", "Opaque paging cursor").option("--query <query>", "Free-text filter").option("--filter-tags <tags>", "Comma-separated tag filter").option("--include-archived", "Include archived memories", false).option("--include-text", "Include memory body text", false).action(async (opts, cmd) => {
2268
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2269
+ const data = await runApi("Fetching feed", async () => {
2270
+ const client = await makeClient(cfg);
2271
+ return client.GET("/me/memories/feed", {
2272
+ params: {
2273
+ query: {
2274
+ limit: Number(opts.limit),
2275
+ includeArchived: Boolean(opts.includeArchived),
2276
+ includeText: Boolean(opts.includeText),
2277
+ ...opts.cursor ? { cursor: opts.cursor } : {},
2278
+ ...opts.query ? { query: opts.query } : {},
2279
+ ...opts.filterTags ? { filterTags: opts.filterTags } : {}
2280
+ }
2281
+ }
2282
+ });
2283
+ });
2284
+ emit(data, cmd.optsWithGlobals().json);
2285
+ });
2286
+ account.command("reviews").description("Your review queue (GET /reviews)").option("--status <status>", "Pending | Resolved | Empty").option("--scope-kind <scopeKind>", "Memory | Project | Workspace").option("--scope-target-id <id>", "Scope target id").option("--limit <n>", "Max results", "20").action(async (opts, cmd) => {
2287
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2288
+ const data = await runApi("Fetching reviews", async () => {
2289
+ const client = await makeClient(cfg);
2290
+ return client.GET("/reviews", {
2291
+ params: {
2292
+ query: {
2293
+ limit: Number(opts.limit),
2294
+ ...opts.status ? { status: opts.status } : {},
2295
+ ...opts.scopeKind ? { scopeKind: opts.scopeKind } : {},
2296
+ ...opts.scopeTargetId ? { scopeTargetId: opts.scopeTargetId } : {}
2297
+ }
2298
+ }
2299
+ });
2300
+ });
2301
+ emit(data, cmd.optsWithGlobals().json);
2302
+ });
2303
+ account.command("review-get <reviewId>").description("Fetch one review bundle (GET /reviews/{reviewId})").action(async (reviewId, _opts, cmd) => {
2304
+ const cfg = resolveConfig(cmd.optsWithGlobals());
2305
+ const data = await runApi("Fetching review", async () => {
2306
+ const client = await makeClient(cfg);
2307
+ return client.GET("/reviews/{reviewId}", { params: { path: { reviewId } } });
1670
2308
  });
1671
2309
  emit(data, cmd.optsWithGlobals().json);
1672
2310
  });
@@ -1775,129 +2413,8 @@ Examples:
1775
2413
 
1776
2414
  // src/setup/apply.ts
1777
2415
  import { createHash as createHash2 } from "crypto";
1778
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1779
- import { dirname as dirname2 } from "path";
1780
-
1781
- // src/setup/operator-surface.ts
1782
- var SectionType = {
1783
- McpConfig: "mcp-config",
1784
- McpConfigToml: "mcp-config-toml",
1785
- InstructionFile: "instruction-file",
1786
- ProjectConfig: "project-config",
1787
- Verify: "verify",
1788
- /** SBC-999 — workspace-pinned conventions, emitted only when the request
1789
- * carried a workspaceId and that workspace has agent-setup-bundle memories. */
1790
- WorkspaceConventions: "workspace-conventions"
1791
- };
1792
- async function fetchSetup(cfg) {
1793
- const client = await makeClient(cfg);
1794
- const { data, error } = await client.GET(
1795
- "/operator-surface/setup",
1796
- cfg.workspaceId ? { params: { query: { workspaceId: cfg.workspaceId } } } : {}
1797
- );
1798
- if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
1799
- return data;
1800
- }
1801
- function findSurface(setup, surfaceKey) {
1802
- return setup.surfaces.find((s) => s.surfaceKey === surfaceKey);
1803
- }
1804
- function findSection(surface, sectionType) {
1805
- return surface?.sections.find((s) => s.sectionType === sectionType);
1806
- }
1807
- function sectionSnippet(section) {
1808
- if (!section) return null;
1809
- for (const step of section.steps) {
1810
- if (step.copyValue) return step.copyValue;
1811
- if (step.codeSnippet) return step.codeSnippet;
1812
- }
1813
- return null;
1814
- }
1815
- function parseTagArtifactId(id) {
1816
- if (!id.startsWith("tag:")) return null;
1817
- const tags = id.slice("tag:".length).split(",").map((t) => t.trim()).filter((t) => t.length > 0);
1818
- return tags.length > 0 ? tags : null;
1819
- }
1820
- async function getPersonalWorkspaceId(cfg) {
1821
- const client = await makeClient(cfg);
1822
- const { data } = await client.GET("/me/personal-workspace", {});
1823
- return data?.workspaceId ?? null;
1824
- }
1825
- async function fetchMemoryFields(cfg, id) {
1826
- const client = await makeClient(cfg);
1827
- const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
1828
- const env = data;
1829
- const m = env?.item ?? env;
1830
- if (!m) return null;
1831
- const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
1832
- return { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
1833
- }
1834
- async function resolveInstruction(cfg, section, personalWorkspaceId) {
1835
- const client = await makeClient(cfg);
1836
- for (const artifact of section.artifacts) {
1837
- const tags = parseTagArtifactId(artifact.id);
1838
- if (!tags) continue;
1839
- const { data } = await client.POST("/memories/search", {
1840
- body: { query: null, textQuery: null, semanticQuery: artifact.title ?? "role instruction template", hybrid: true, limit: 1, includeArchived: false, includeSystem: false, tags }
1841
- });
1842
- const hits = data ?? [];
1843
- if (hits.length === 0) continue;
1844
- const templateId = hits[0].id;
1845
- const template = await fetchMemoryFields(cfg, templateId);
1846
- if (typeof template?.text !== "string" || template.text.length === 0) continue;
1847
- const templateTags = template.tags ?? tags;
1848
- if (personalWorkspaceId) {
1849
- const { data: ovr } = await client.POST("/memories/search", {
1850
- body: { query: null, textQuery: null, semanticQuery: "role override", hybrid: true, limit: 1, includeArchived: false, includeSystem: false, tags: ["sechroom:role:override", `sechroom:template-ref:${templateId}`], owner: { type: "Workspace", id: personalWorkspaceId } }
1851
- });
1852
- const ovrHits = ovr ?? [];
1853
- if (ovrHits.length > 0) {
1854
- const override = await fetchMemoryFields(cfg, ovrHits[0].id);
1855
- if (typeof override?.text === "string" && override.text.length > 0) {
1856
- return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
1857
- }
1858
- }
1859
- }
1860
- return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
1861
- }
1862
- return null;
1863
- }
1864
- async function resolveWorkspaceConventions(cfg, section) {
1865
- const parts = [];
1866
- const refs = [];
1867
- for (const artifact of section.artifacts) {
1868
- if (parseTagArtifactId(artifact.id)) continue;
1869
- const mem = await fetchMemoryFields(cfg, artifact.id);
1870
- if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
1871
- parts.push(mem.text.trim());
1872
- refs.push(`${artifact.id}@v${mem.version ?? 1}`);
1873
- }
1874
- }
1875
- if (parts.length === 0) return null;
1876
- return { body: parts.join("\n\n---\n\n"), refs };
1877
- }
1878
- async function createOverride(cfg, template, personalWorkspaceId) {
1879
- const client = await makeClient(cfg);
1880
- const overrideTags = template.templateTags.filter(
1881
- (t) => t !== "sechroom:role:template" && !t.startsWith("sechroom:bundle:") && !t.startsWith("sechroom:template-ref:")
1882
- );
1883
- overrideTags.push("sechroom:role:override", `sechroom:template-ref:${template.templateId}`);
1884
- const { error } = await client.POST("/memories", {
1885
- body: {
1886
- text: template.body,
1887
- type: "reference",
1888
- content: "{}",
1889
- confidence: 1,
1890
- source: "cli-agent-instructions-customize",
1891
- archetype: "Document",
1892
- title: template.title ?? null,
1893
- tags: overrideTags,
1894
- owner: { type: "Workspace", id: personalWorkspaceId }
1895
- }
1896
- });
1897
- if (error) throw new Error(`creating personal copy failed: ${JSON.stringify(error)}`);
1898
- }
1899
-
1900
- // src/setup/apply.ts
2416
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2417
+ import { dirname as dirname5 } from "path";
1901
2418
  var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
1902
2419
  var MARKER_END = "<!-- @sechroom/cli:end";
1903
2420
  function normalizeBody(s) {
@@ -1950,22 +2467,22 @@ function parseManagedBlock(content, block) {
1950
2467
  return null;
1951
2468
  }
1952
2469
  function ensureDir2(path) {
1953
- mkdirSync2(dirname2(path), { recursive: true });
2470
+ mkdirSync4(dirname5(path), { recursive: true });
1954
2471
  }
1955
2472
  function readOr(path, fallback) {
1956
2473
  try {
1957
- return readFileSync2(path, "utf8");
2474
+ return readFileSync4(path, "utf8");
1958
2475
  } catch {
1959
2476
  return fallback;
1960
2477
  }
1961
2478
  }
1962
2479
  function mergeMcpJson(path, snippet, dryRun) {
1963
2480
  const incoming = JSON.parse(snippet);
1964
- const existed = existsSync2(path);
2481
+ const existed = existsSync5(path);
1965
2482
  let current = {};
1966
2483
  if (existed) {
1967
2484
  try {
1968
- current = JSON.parse(readFileSync2(path, "utf8"));
2485
+ current = JSON.parse(readFileSync4(path, "utf8"));
1969
2486
  } catch {
1970
2487
  return { kind: "mcp", path, status: "skipped", note: "existing file isn't valid JSON \u2014 left untouched" };
1971
2488
  }
@@ -1973,26 +2490,26 @@ function mergeMcpJson(path, snippet, dryRun) {
1973
2490
  current.mcpServers = { ...current.mcpServers ?? {}, ...incoming.mcpServers ?? {} };
1974
2491
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
1975
2492
  ensureDir2(path);
1976
- writeFileSync2(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
2493
+ writeFileSync4(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
1977
2494
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
1978
2495
  }
1979
2496
  function mergeCodexToml(path, snippet, dryRun) {
1980
- const existed = existsSync2(path);
2497
+ const existed = existsSync5(path);
1981
2498
  let body = readOr(path, "");
1982
2499
  body = body.replace(/(^|\n)\[mcp_servers\.sechroom\][^[]*/, "\n").replace(/\n{3,}/g, "\n\n");
1983
2500
  const trimmed = body.trim();
1984
2501
  const next = (trimmed.length > 0 ? trimmed + "\n\n" : "") + snippet.trim() + "\n";
1985
2502
  if (dryRun) return { kind: "mcp", path, status: "dry-run" };
1986
2503
  ensureDir2(path);
1987
- writeFileSync2(path, next, { mode: 384 });
2504
+ writeFileSync4(path, next, { mode: 384 });
1988
2505
  return { kind: "mcp", path, status: existed ? "merged" : "created" };
1989
2506
  }
1990
2507
  function writeInstructionBlock(path, write, dryRun) {
1991
- const existed = existsSync2(path);
2508
+ const existed = existsSync5(path);
1992
2509
  const next = computeBlockFile(readOr(path, ""), write);
1993
2510
  if (dryRun) return { kind: "instruction", path, status: "dry-run" };
1994
2511
  ensureDir2(path);
1995
- writeFileSync2(path, next);
2512
+ writeFileSync4(path, next);
1996
2513
  return { kind: "instruction", path, status: existed ? "merged" : "created" };
1997
2514
  }
1998
2515
  function computeBlockFile(current, write) {
@@ -2033,7 +2550,7 @@ function applyBlock(path, write, mode, dryRun) {
2033
2550
  const next = computeBlockFile(current, write);
2034
2551
  if (!dryRun) {
2035
2552
  ensureDir2(proposedPath);
2036
- writeFileSync2(proposedPath, next);
2553
+ writeFileSync4(proposedPath, next);
2037
2554
  }
2038
2555
  return {
2039
2556
  kind: "instruction",
@@ -2103,105 +2620,127 @@ async function applyClient(cfg, setup, target, opts) {
2103
2620
  return actions;
2104
2621
  }
2105
2622
 
2106
- // src/setup/clients.ts
2107
- import { existsSync as existsSync3 } from "fs";
2108
- import { homedir as homedir2 } from "os";
2109
- import { dirname as dirname3, join as join2 } from "path";
2110
- function claudeDesktopConfigPath(home) {
2111
- switch (process.platform) {
2112
- case "darwin":
2113
- return join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
2114
- case "win32":
2115
- return join2(process.env.APPDATA ?? join2(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
2116
- default:
2117
- return join2(home, ".config", "Claude", "claude_desktop_config.json");
2118
- }
2119
- }
2120
- function clientTargets(cwd) {
2121
- const home = homedir2();
2122
- return {
2123
- "claude-code": {
2124
- key: "claude-code",
2125
- label: "Claude Code",
2126
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join2(cwd, ".mcp.json"), format: "json" },
2127
- instruction: { surfaceKey: "claude-code", path: join2(cwd, "CLAUDE.md") }
2128
- },
2129
- "claude-desktop": {
2130
- key: "claude-desktop",
2131
- label: "Claude Desktop",
2132
- mcp: { surfaceKey: "claude-desktop", sectionType: SectionType.McpConfig, path: claudeDesktopConfigPath(home), format: "json" },
2133
- instruction: { surfaceKey: "claude-desktop", path: join2(home, ".claude", "CLAUDE.md") }
2134
- },
2135
- codex: {
2136
- key: "codex",
2137
- label: "Codex CLI",
2138
- mcp: { surfaceKey: "chatgpt", sectionType: SectionType.McpConfigToml, path: join2(home, ".codex", "config.toml"), format: "toml" },
2139
- instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
2140
- },
2141
- cursor: {
2142
- key: "cursor",
2143
- label: "Cursor",
2144
- mcp: { surfaceKey: "claude-code", sectionType: SectionType.McpConfig, path: join2(cwd, ".cursor", "mcp.json"), format: "json" },
2145
- instruction: { surfaceKey: "chatgpt", path: join2(cwd, "AGENTS.md") }
2623
+ // src/setup/hooks-offer.ts
2624
+ import { homedir as homedir4 } from "os";
2625
+ async function maybeOfferHooks(opts) {
2626
+ if (opts.dryRun) return;
2627
+ const cwd = opts.cwd ?? process.cwd();
2628
+ const surfaces = detectHookSurfaces(cwd);
2629
+ if (surfaces.length === 0) return;
2630
+ const names = surfaces.map((s) => HOOK_SURFACE_LABEL[s]).join(" + ");
2631
+ process.stderr.write(
2632
+ `
2633
+ Sechroom can wire continuity lifecycle hooks into ${style.bold(names)} so your agent
2634
+ auto-resumes where you left off and checkpoints working state before compacting.
2635
+ `
2636
+ );
2637
+ const install = opts.yes ? true : canPrompt() ? await promptYesNo(`Install the continuity hooks for ${names}?`) : false;
2638
+ if (!install) return;
2639
+ try {
2640
+ const installed = installHookSurfaces(surfaces, { dryRun: false, cwd, home: homedir4() });
2641
+ let changed = false;
2642
+ for (const { surface, results } of installed) {
2643
+ for (const r of results) {
2644
+ if (r.status !== "current") changed = true;
2645
+ const verb = r.status === "current" ? "already configured" : r.status === "created" ? "created" : "updated";
2646
+ process.stderr.write(`${style.green("\u2713")} ${HOOK_SURFACE_LABEL[surface]}: ${r.path} (${verb})
2647
+ `);
2648
+ }
2146
2649
  }
2147
- };
2148
- }
2149
- var ALL_CLIENT_KEYS = ["claude-code", "claude-desktop", "codex", "cursor"];
2150
- var DEFAULT_CLIENT_KEY = "claude-code";
2151
- function detectInstalledClients(cwd) {
2152
- const home = homedir2();
2153
- const detected = [];
2154
- if (existsSync3(join2(home, ".claude"))) detected.push("claude-code");
2155
- if (existsSync3(dirname3(claudeDesktopConfigPath(home)))) detected.push("claude-desktop");
2156
- if (existsSync3(join2(home, ".codex"))) detected.push("codex");
2157
- if (existsSync3(join2(home, ".cursor")) || existsSync3(join2(cwd, ".cursor"))) detected.push("cursor");
2158
- return detected;
2650
+ if (changed) {
2651
+ process.stderr.write(`${style.dim("Restart (or reload) your agent for the hooks to take effect.")}
2652
+ `);
2653
+ }
2654
+ warnIfSechroomNotOnPath();
2655
+ } catch (err2) {
2656
+ process.stderr.write(`${style.dim(`(skipped hook install: ${err2.message})`)}
2657
+ `);
2658
+ }
2159
2659
  }
2160
2660
 
2161
2661
  // src/setup/skills-offer.ts
2162
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
2163
- import { homedir as homedir3 } from "os";
2164
- import { join as join4 } from "path";
2662
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
2663
+ import { homedir as homedir5 } from "os";
2664
+ import { join as join5 } from "path";
2165
2665
 
2166
- // src/sem.ts
2167
- import { dirname as dirname4, join as join3 } from "path";
2168
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2169
- var SEM_FILE = ".sem";
2170
- function localSemPath(cwd = process.cwd()) {
2171
- return join3(cwd, SEM_FILE);
2666
+ // src/setup/lane-pin.ts
2667
+ var CODE_LANE_PREFIX_BY_CLIENT = {
2668
+ "claude-code": "claude-code",
2669
+ "claude-desktop": "claude-code",
2670
+ cursor: "claude-code",
2671
+ codex: "codex"
2672
+ };
2673
+ var CLIENT_PRIORITY = ["claude-code", "claude-desktop", "cursor", "codex"];
2674
+ function handleFromDisplayName(name) {
2675
+ if (!name) return void 0;
2676
+ const localPart = name.trim().split("@")[0] ?? "";
2677
+ const first = localPart.split(/[\s._-]+/)[0]?.toLowerCase().replace(/[^a-z0-9]/g, "");
2678
+ return first || void 0;
2172
2679
  }
2173
- function resolveSemPathForRead(start = process.cwd()) {
2174
- let dir = start;
2175
- while (true) {
2176
- const candidate = join3(dir, SEM_FILE);
2177
- if (existsSync4(candidate)) return candidate;
2178
- const parent = dirname4(dir);
2179
- if (parent === dir) return void 0;
2180
- dir = parent;
2181
- }
2680
+ function codeLanePrefix(clients) {
2681
+ for (const c of CLIENT_PRIORITY) if (clients.includes(c)) return CODE_LANE_PREFIX_BY_CLIENT[c];
2682
+ return "claude-code";
2182
2683
  }
2183
- function parseSem(text) {
2184
- const out = {};
2185
- for (const raw of text.split("\n")) {
2186
- const line = raw.trim();
2187
- if (!line || line.startsWith("#")) continue;
2188
- const eq = line.indexOf("=");
2189
- if (eq === -1) continue;
2190
- const key = line.slice(0, eq).trim();
2191
- const value = line.slice(eq + 1).trim();
2192
- if (key) out[key] = value;
2684
+ async function inferLanes(cfg, clients) {
2685
+ let wf;
2686
+ let profile;
2687
+ try {
2688
+ const client = await makeClient(cfg);
2689
+ [wf, profile] = await Promise.all([
2690
+ client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0),
2691
+ client.GET("/me/profile", {}).then((r) => r.data).catch(() => void 0)
2692
+ ]);
2693
+ } catch {
2193
2694
  }
2194
- return out;
2695
+ const handle = handleFromDisplayName(profile?.effectiveDisplayName);
2696
+ const prefix = codeLanePrefix(clients ?? ["claude-code"]);
2697
+ return {
2698
+ code: process.env.SECHROOM_CODE_LANE ?? wf?.defaultCodeLane ?? (handle ? `${prefix}-${handle}` : void 0),
2699
+ design: process.env.SECHROOM_DESIGN_LANE ?? wf?.defaultDesignLane ?? (handle ? `claude-design-${handle}` : void 0)
2700
+ };
2195
2701
  }
2196
- function serializeSem(values) {
2197
- const header = "# sechroom lane pin (per-location fallback) \u2014 resolved at runtime by operator skills.\n";
2198
- const body = Object.entries(values).map(([k, v]) => `${k} = ${v}`).join("\n");
2199
- return header + body + "\n";
2702
+ function writePin(code, design) {
2703
+ const values = {};
2704
+ if (code) values["code-lane"] = code;
2705
+ if (design) values["design-lane"] = design;
2706
+ if (Object.keys(values).length === 0) return;
2707
+ const target = writeSem(values);
2708
+ process.stderr.write(`${ok("\u2713")} lane pin written \u2192 ${target} ${style.dim("(./.sechroom/lane.json, git-ignored)")}
2709
+ `);
2200
2710
  }
2201
- function readSem(path) {
2202
- const p = path ?? resolveSemPathForRead();
2203
- if (!p || !existsSync4(p)) return void 0;
2204
- return { path: p, values: parseSem(readFileSync3(p, "utf8")) };
2711
+ async function ensureLanePin(cfg, opts) {
2712
+ if (opts.dryRun) return;
2713
+ if (readSem()) return;
2714
+ const { code: codeGuess, design: designGuess } = await inferLanes(cfg, opts.clients);
2715
+ if (!canPrompt() || opts.yes) {
2716
+ if (opts.yes && (codeGuess || designGuess)) writePin(codeGuess, designGuess);
2717
+ return;
2718
+ }
2719
+ if (codeGuess || designGuess) {
2720
+ process.stderr.write(
2721
+ `
2722
+ I can pin this checkout's lane so operator skills + the continuity hook resolve your identity:
2723
+ `
2724
+ );
2725
+ if (codeGuess) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(codeGuess)}
2726
+ `);
2727
+ if (designGuess) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(designGuess)}
2728
+ `);
2729
+ if (await promptYesNo("Pin these?")) {
2730
+ writePin(codeGuess, designGuess);
2731
+ return;
2732
+ }
2733
+ }
2734
+ const code = await promptText("Code-lane id (e.g. claude-code-you, blank to skip)?", codeGuess ?? "");
2735
+ const design = await promptText("Design-lane id (e.g. claude-design-you, blank to skip)?", designGuess ?? "");
2736
+ if (!code && !design) {
2737
+ process.stderr.write(
2738
+ ` ${style.dim("skipped \u2014 set later with")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")}
2739
+ `
2740
+ );
2741
+ return;
2742
+ }
2743
+ writePin(code || void 0, design || void 0);
2205
2744
  }
2206
2745
 
2207
2746
  // src/setup/skills-offer.ts
@@ -2242,43 +2781,19 @@ async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
2242
2781
  Found ${style.bold(String(names.length))} operator skill(s) installed in your workspace: ${names.join(", ")}.
2243
2782
  `
2244
2783
  );
2245
- const dir = join4(homedir3(), ".claude", "skills");
2784
+ const dir = join5(homedir5(), ".claude", "skills");
2246
2785
  const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dir}/ so ${surface} can use them?`) : false;
2247
2786
  if (!materialise) return;
2248
2787
  const written = [];
2249
2788
  for (const [name, m] of byName) {
2250
2789
  const body = m.text ?? m.Text ?? "";
2251
- mkdirSync3(join4(dir, name), { recursive: true });
2252
- writeFileSync3(join4(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
2790
+ mkdirSync5(join5(dir, name), { recursive: true });
2791
+ writeFileSync5(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
2253
2792
  written.push(name);
2254
2793
  }
2255
2794
  process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${dir}
2256
2795
  `);
2257
- if (readSem()) return;
2258
- const setLane = opts.yes ? false : canPrompt() ? await promptYesNo("Set your lane now so the skills can resolve their identity slots?") : false;
2259
- if (!setLane) {
2260
- process.stderr.write(
2261
- ` ${style.dim("run")} ${style.cyan("sechroom skills set-lane --code-lane \u2026 --design-lane \u2026")} ${style.dim("when ready.")}
2262
- `
2263
- );
2264
- return;
2265
- }
2266
- let wf;
2267
- try {
2268
- const client = await makeClient(cfg);
2269
- wf = await client.GET("/me/workflow-preferences", {}).then((r) => r.data).catch(() => void 0);
2270
- } catch {
2271
- }
2272
- const code = await promptText("Code-lane id (e.g. claude-code-you)?", wf?.defaultCodeLane);
2273
- const design = await promptText("Design-lane id (e.g. claude-design-you)?", wf?.defaultDesignLane);
2274
- const values = {};
2275
- if (code) values["code-lane"] = code;
2276
- if (design) values["design-lane"] = design;
2277
- if (Object.keys(values).length === 0) return;
2278
- const target = localSemPath();
2279
- writeFileSync3(target, serializeSem(values));
2280
- process.stderr.write(`${style.green("\u2713")} lane pin written \u2192 ${target}
2281
- `);
2796
+ await ensureLanePin(cfg, { yes: opts.yes, dryRun: opts.dryRun, clients: [surface] });
2282
2797
  }
2283
2798
 
2284
2799
  // src/commands/setup.ts
@@ -2370,6 +2885,9 @@ Examples:
2370
2885
  if (!json && !opts.dryRun && !opts.mcpOnly) {
2371
2886
  await maybeOfferSkills(cfg, personalWorkspaceId, { yes: false, dryRun: Boolean(opts.dryRun), surface: "claude-code" });
2372
2887
  }
2888
+ if (!json && !opts.dryRun && !opts.mcpOnly) {
2889
+ await maybeOfferHooks({ yes: false, dryRun: Boolean(opts.dryRun), cwd: process.cwd() });
2890
+ }
2373
2891
  if (json) {
2374
2892
  emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
2375
2893
  return;
@@ -2389,36 +2907,147 @@ Next \u2014 verify: ${verify.description}
2389
2907
  }
2390
2908
  function registerSetup(program2) {
2391
2909
  const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
2392
- setup.command("mcp <client>").description(`Write only the MCP config for a client (${ALL_CLIENT_KEYS.join(", ")})`).option("--dry-run", "print what would be written without writing", false).action(async (client, opts, cmd) => {
2393
- await runSingle(client, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
2910
+ setup.command("mcp <clients...>").description(`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).addHelpText("after", "\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all").action(async (clients, opts, cmd) => {
2911
+ await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: true, agentFiles: false });
2394
2912
  });
2395
- setup.command("agent-files <client>").description(`Write only the agent instruction file for a client (${ALL_CLIENT_KEYS.join(", ")})`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").action(async (client, opts, cmd) => {
2396
- await runSingle(client, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy });
2913
+ setup.command("agent-files <clients...>").description(`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`).option("--dry-run", "print what would be written without writing", false).option("--copy", "make a personal copy you can edit (default: prompt on a TTY, else skip)").addHelpText("after", "\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all").action(async (clients, opts, cmd) => {
2914
+ await runClients(clients, cmd, { dryRun: Boolean(opts.dryRun), mcp: false, agentFiles: true, copy: opts.copy });
2397
2915
  });
2398
2916
  }
2399
- async function runSingle(client, cmd, opts) {
2917
+ async function runClients(clients, cmd, opts) {
2400
2918
  const cfg = resolveConfig(cmd.optsWithGlobals());
2401
2919
  const targets = clientTargets(process.cwd());
2402
- const target = targets[client];
2403
- if (!target) fail(`unknown client '${client}'. Known: ${ALL_CLIENT_KEYS.join(", ")}.`);
2920
+ const keys = resolveClientKeys(clients.join(","));
2404
2921
  const setupData = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
2405
2922
  const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
2406
2923
  if (opts.agentFiles && !opts.dryRun) {
2407
- await maybeOfferCopies(cfg, setupData, targets, [client], personalWorkspaceId, copyChoice(opts));
2924
+ await maybeOfferCopies(cfg, setupData, targets, keys, personalWorkspaceId, copyChoice(opts));
2408
2925
  }
2409
- const actions = await applyClient(cfg, setupData, target, {
2410
- dryRun: opts.dryRun,
2411
- mcp: opts.mcp,
2412
- agentFiles: opts.agentFiles,
2413
- personalWorkspaceId
2414
- });
2415
2926
  const json = cmd.optsWithGlobals().json;
2927
+ const result = [];
2928
+ for (const key of keys) {
2929
+ const target = targets[key];
2930
+ const actions = await applyClient(cfg, setupData, target, {
2931
+ dryRun: opts.dryRun,
2932
+ mcp: opts.mcp,
2933
+ agentFiles: opts.agentFiles,
2934
+ personalWorkspaceId
2935
+ });
2936
+ result.push({ client: key, actions });
2937
+ if (!json) printActions(target, actions);
2938
+ }
2416
2939
  if (json) {
2417
- emit({ dryRun: opts.dryRun, client, actions }, true);
2418
- } else {
2419
- printActions(target, actions);
2420
- process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
2940
+ emit({ dryRun: opts.dryRun, clients: result }, true);
2941
+ return;
2942
+ }
2943
+ process.stdout.write(opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n");
2944
+ }
2945
+
2946
+ // src/commands/onboard.ts
2947
+ import { existsSync as existsSync7 } from "fs";
2948
+ import { join as join7 } from "path";
2949
+
2950
+ // src/commands/fanout.ts
2951
+ import { spawnSync } from "child_process";
2952
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync, statSync } from "fs";
2953
+ import { isAbsolute, join as join6, resolve } from "path";
2954
+ var ICON = {
2955
+ refresh: "\u21BB",
2956
+ bind: "+",
2957
+ "skip-missing": "\u2013",
2958
+ "skip-unbound": "\u26A0"
2959
+ };
2960
+ function resolveChildDir(path, root) {
2961
+ return isAbsolute(path) ? path : resolve(root, path);
2962
+ }
2963
+ function discoverChildren(root) {
2964
+ let names;
2965
+ try {
2966
+ names = readdirSync(root);
2967
+ } catch {
2968
+ return [];
2969
+ }
2970
+ const out = [];
2971
+ for (const name of names.sort()) {
2972
+ if (name.startsWith(".") || name === "node_modules") continue;
2973
+ const dir = join6(root, name);
2974
+ try {
2975
+ if (!statSync(dir).isDirectory()) continue;
2976
+ } catch {
2977
+ continue;
2978
+ }
2979
+ if (existsSync6(join6(dir, ".git")) || committedBindingPath(dir)) out.push(name);
2980
+ }
2981
+ return out;
2982
+ }
2983
+ function readManifest(path) {
2984
+ if (!existsSync6(path)) return null;
2985
+ let parsed;
2986
+ try {
2987
+ parsed = JSON.parse(readFileSync5(path, "utf8"));
2988
+ } catch (err2) {
2989
+ throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
2990
+ }
2991
+ return Array.isArray(parsed.repos) ? parsed.repos.filter((r) => r && typeof r.path === "string") : [];
2992
+ }
2993
+ function passthroughGlobals(g) {
2994
+ const out = [];
2995
+ if (g.baseUrl) out.push("--base-url", g.baseUrl);
2996
+ if (g.tenant) out.push("--tenant", g.tenant);
2997
+ return out;
2998
+ }
2999
+ function runChildren(plans, o) {
3000
+ const { globals, dryRun, json } = o;
3001
+ const results = [];
3002
+ for (const plan of plans) {
3003
+ const runs = plan.argv.length > 0;
3004
+ const argv = runs ? [...globals, ...plan.argv] : [];
3005
+ if (!json) {
3006
+ process.stderr.write(` ${ICON[plan.disposition]} ${style.cyan(plan.label)} ${style.dim(plan.reason)}
3007
+ `);
3008
+ if (runs && dryRun) process.stderr.write(` ${style.dim(`would run: sechroom ${argv.join(" ")}`)}
3009
+ `);
3010
+ }
3011
+ let exitCode = runs ? 0 : null;
3012
+ if (runs && !dryRun) {
3013
+ const res = spawnSync(process.execPath, [process.argv[1], ...argv], {
3014
+ cwd: plan.dir,
3015
+ stdio: json ? "ignore" : "inherit"
3016
+ });
3017
+ exitCode = res.status;
3018
+ if (!json) {
3019
+ process.stderr.write(
3020
+ exitCode === 0 ? ` ${ok("\u2713")} ${style.dim("onboard ok")}
3021
+ ` : ` ${warn("\u2717")} ${style.dim(`onboard exited ${exitCode ?? "signal"}`)}
3022
+ `
3023
+ );
3024
+ }
3025
+ }
3026
+ results.push({
3027
+ path: plan.label,
3028
+ dir: plan.dir,
3029
+ disposition: plan.disposition,
3030
+ ran: runs && !dryRun,
3031
+ exitCode,
3032
+ reason: plan.reason
3033
+ });
2421
3034
  }
3035
+ return results;
3036
+ }
3037
+ function summarizeFanout(results, o) {
3038
+ const ran = results.filter((r) => r.ran);
3039
+ const failed = ran.filter((r) => r.exitCode !== 0);
3040
+ const skipped = results.filter((r) => r.disposition.startsWith("skip"));
3041
+ const wouldRun = results.filter((r) => !r.disposition.startsWith("skip"));
3042
+ const tally = (o.dryRun ? [wouldRun.length ? `${wouldRun.length} would onboard` : null, skipped.length ? `${skipped.length} would skip` : null] : [
3043
+ ran.length ? `${ran.length - failed.length}/${ran.length} onboarded` : null,
3044
+ skipped.length ? `${skipped.length} skipped` : null,
3045
+ failed.length ? `${failed.length} failed` : null
3046
+ ]).filter(Boolean).join(", ");
3047
+ process.stderr.write(`
3048
+ ${failed.length ? warn("\u26A0") : ok("\u2713")} ${tally || "nothing to do"}${o.dryRun ? style.dim(" (dry run)") : ""}
3049
+ `);
3050
+ if (failed.length) process.exit(1);
2422
3051
  }
2423
3052
 
2424
3053
  // src/commands/onboard.ts
@@ -2493,7 +3122,7 @@ async function warnIfProjectStray(client, projectId, workspaceId, json) {
2493
3122
  );
2494
3123
  }
2495
3124
  }
2496
- async function pickWorkspace(client) {
3125
+ async function pickWorkspace(client, promptLabel = "Bind this directory to a workspace:") {
2497
3126
  const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
2498
3127
  if (all.length === 0) {
2499
3128
  process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
@@ -2516,7 +3145,7 @@ async function pickWorkspace(client) {
2516
3145
  ...pool.slice().sort((a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId))).map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
2517
3146
  { label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
2518
3147
  ];
2519
- const chosen = await promptSelect("Bind this directory to a workspace:", choices, SKIP);
3148
+ const chosen = await promptSelect(promptLabel, choices, SKIP);
2520
3149
  if (chosen === SKIP) return void 0;
2521
3150
  const picked = byId.get(chosen);
2522
3151
  const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
@@ -2599,7 +3228,7 @@ async function ensureTenant(baseUrl, g, opts) {
2599
3228
  "Where should this tenant + base URL be saved?",
2600
3229
  [
2601
3230
  { label: "Globally", value: "global", hint: "all projects on this machine" },
2602
- { label: "This directory", value: "local", hint: ".sechroom.json \u2014 project + subdirs" }
3231
+ { label: "This directory", value: "local", hint: ".sechroom.json \u2014 committed, project + subdirs" }
2603
3232
  ],
2604
3233
  local.path ? "local" : "global"
2605
3234
  ) === "local";
@@ -2674,16 +3303,120 @@ async function chooseClients(clientFlag, yes, cwd) {
2674
3303
  );
2675
3304
  return picks.length > 0 ? picks : preselected;
2676
3305
  }
3306
+ async function planRecurseChild(entry, root, client, opts) {
3307
+ const dir = resolveChildDir(entry.path, root);
3308
+ if (!existsSync7(dir)) {
3309
+ return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
3310
+ }
3311
+ if (existsSync7(join7(dir, ".sechroom.json"))) {
3312
+ return {
3313
+ label: entry.path,
3314
+ dir,
3315
+ disposition: "refresh",
3316
+ argv: ["onboard", "--refresh", "--yes"],
3317
+ reason: "bound (committed .sechroom.json) \u2014 refresh in place"
3318
+ };
3319
+ }
3320
+ if (entry.workspaceId) {
3321
+ return {
3322
+ label: entry.path,
3323
+ dir,
3324
+ disposition: "bind",
3325
+ argv: ["onboard", "--yes", "--local", "--workspace", entry.workspaceId],
3326
+ reason: `unbound \u2014 bind to ${entry.workspaceId}`
3327
+ };
3328
+ }
3329
+ if (opts.dryRun) {
3330
+ return { label: entry.path, dir, disposition: "bind", argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"], reason: "unbound \u2014 would prompt for a workspace" };
3331
+ }
3332
+ if (opts.yes || !canPrompt()) {
3333
+ return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)" };
3334
+ }
3335
+ process.stderr.write(`
3336
+ ${style.bold(entry.path)} ${style.dim("is not bound yet.")}
3337
+ `);
3338
+ const ws = await pickWorkspace(client, `Bind ${style.cyan(entry.path)} to a workspace:`);
3339
+ if (!ws) {
3340
+ return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
3341
+ }
3342
+ return {
3343
+ label: entry.path,
3344
+ dir,
3345
+ disposition: "bind",
3346
+ argv: ["onboard", "--yes", "--local", "--workspace", ws],
3347
+ reason: `unbound \u2014 bind to ${ws}`
3348
+ };
3349
+ }
3350
+ async function resolveFanoutLane(cfg, opts) {
3351
+ let code = opts.lane ?? process.env.SECHROOM_CODE_LANE;
3352
+ let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
3353
+ if (!code || !design) {
3354
+ const clients = detectInstalledClients(process.cwd());
3355
+ const inferred = await inferLanes(cfg, clients.length ? clients : void 0);
3356
+ code = code ?? inferred.code;
3357
+ design = design ?? inferred.design;
3358
+ }
3359
+ if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
3360
+ process.stderr.write(`
3361
+ This fan-out will pin the same lane in every repo:
3362
+ `);
3363
+ if (code) process.stderr.write(` ${style.dim("code-lane")} = ${style.cyan(code)}
3364
+ `);
3365
+ if (design) process.stderr.write(` ${style.dim("design-lane")} = ${style.cyan(design)}
3366
+ `);
3367
+ if (!await promptYesNo("Use this lane for all repos?")) {
3368
+ code = await promptText("Code-lane id (blank = let each repo infer)?", code ?? "") || void 0;
3369
+ design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
3370
+ }
3371
+ }
3372
+ if (code) process.env.SECHROOM_CODE_LANE = code;
3373
+ else delete process.env.SECHROOM_CODE_LANE;
3374
+ if (design) process.env.SECHROOM_DESIGN_LANE = design;
3375
+ else delete process.env.SECHROOM_DESIGN_LANE;
3376
+ return { code, design };
3377
+ }
3378
+ async function runRecurse(cfg, g, opts) {
3379
+ const { yes, dryRun, json } = opts;
3380
+ const root = process.cwd();
3381
+ const manifestPath = join7(root, ".sechroom", "repos.json");
3382
+ const fromManifest = readManifest(manifestPath);
3383
+ const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
3384
+ const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
3385
+ if (entries.length === 0) {
3386
+ if (json) process.stdout.write(JSON.stringify({ recurse: true, root, repos: [] }) + "\n");
3387
+ else process.stderr.write(`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
3388
+ `);
3389
+ return;
3390
+ }
3391
+ if (!json) {
3392
+ process.stderr.write(`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
3393
+ `);
3394
+ }
3395
+ const lane = await resolveFanoutLane(cfg, { lane: opts.lane, designLane: opts.designLane, yes, dryRun });
3396
+ if (!json && lane.code) process.stderr.write(`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
3397
+ `);
3398
+ const client = await makeClient(cfg);
3399
+ const plans = [];
3400
+ for (const entry of entries) plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
3401
+ const results = runChildren(plans, { globals: passthroughGlobals(g), dryRun, json });
3402
+ if (json) {
3403
+ process.stdout.write(JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n");
3404
+ return;
3405
+ }
3406
+ summarizeFanout(results, { dryRun });
3407
+ }
2677
3408
  function registerOnboard(program2) {
2678
- program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save tenant + base URL to a directory-local .sechroom.json instead of the global config", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
3409
+ program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
2679
3410
  "after",
2680
3411
  `
2681
3412
  Examples:
2682
3413
  $ sechroom onboard guided, interactive (asks where to save config + how to wire)
2683
3414
  $ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
2684
3415
  $ sechroom onboard --no-mcp agent instructions only, skip MCP config
2685
- $ sechroom onboard --local save tenant + base URL to ./.sechroom.json
3416
+ $ sechroom onboard --local save tenant + base URL to a committed ./.sechroom.json
2686
3417
  $ sechroom onboard --workspace wsp_XX bind this directory to a workspace (no pick prompt)
3418
+ $ sechroom onboard --recurse orchestration root: onboard every child repo under this dir
3419
+ $ sechroom onboard --recurse --lane claude-code-you pin one lane across every repo in the tree
2687
3420
  $ sechroom onboard --refresh refresh out-of-date instruction blocks in place
2688
3421
  $ sechroom onboard --check CI/pre-commit: nonzero exit if instructions are out of date
2689
3422
  $ sechroom onboard --yes non-interactive: defaults + global config + full wire
@@ -2695,6 +3428,15 @@ Examples:
2695
3428
  const mode = opts.check ? "check" : opts.force ? "force" : "apply";
2696
3429
  const check = mode === "check";
2697
3430
  const yes = Boolean(opts.yes) || check;
3431
+ if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
3432
+ if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
3433
+ if (opts.recurse) {
3434
+ const baseUrl2 = resolveBaseUrl(g);
3435
+ await ensureAuth({ baseUrl: baseUrl2, tenant: "", clientId: readPersisted().clientId }, yes);
3436
+ const cfg2 = await ensureTenant(baseUrl2, g, { yes: true, json, persist: false });
3437
+ await runRecurse(cfg2, g, { yes, dryRun, json, lane: opts.lane, designLane: opts.designLane });
3438
+ return;
3439
+ }
2698
3440
  const baseUrl = resolveBaseUrl(g);
2699
3441
  await ensureAuth({ baseUrl, tenant: "", clientId: readPersisted().clientId }, yes);
2700
3442
  const cfg = await ensureTenant(baseUrl, g, { yes, json, local: Boolean(opts.local), workspace: opts.workspace, persist: !check });
@@ -2712,12 +3454,17 @@ Examples:
2712
3454
  emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
2713
3455
  return;
2714
3456
  }
3457
+ if (!dryRun) {
3458
+ await ensureLanePin(cfg, { yes, dryRun, clients: detectInstalledClients(process.cwd()) });
3459
+ await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
3460
+ }
2715
3461
  process.stdout.write(
2716
3462
  `
2717
3463
  ${style.bold("Done.")} The CLI is configured for ${style.cyan(cfg.tenant)} \u2014 no AI-client files written.
2718
3464
  Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom --help")}
2719
3465
  `
2720
3466
  );
3467
+ await printStarterPrompt("cli");
2721
3468
  return;
2722
3469
  }
2723
3470
  const keys = await chooseClients(opts.client, yes, process.cwd());
@@ -2762,9 +3509,15 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
2762
3509
  }
2763
3510
  process.exit(wouldChange === 0 ? 0 : 1);
2764
3511
  }
3512
+ if (!json && !dryRun) {
3513
+ await ensureLanePin(cfg, { yes, dryRun, clients: keys });
3514
+ }
2765
3515
  if (!json && !dryRun) {
2766
3516
  await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
2767
3517
  }
3518
+ if (!json && !dryRun) {
3519
+ await maybeOfferHooks({ yes, dryRun, cwd: process.cwd() });
3520
+ }
2768
3521
  if (json) {
2769
3522
  emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, eval: evalCounts, clients: result }, true);
2770
3523
  return;
@@ -2790,6 +3543,7 @@ ${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new
2790
3543
  ${style.bold("Done.")} Agent instructions written (no MCP config).
2791
3544
  `
2792
3545
  );
3546
+ if (!dryRun) await printStarterPrompt("agent", cfg);
2793
3547
  });
2794
3548
  }
2795
3549
  async function chooseWire(opts, yes) {
@@ -2807,16 +3561,142 @@ async function chooseWire(opts, yes) {
2807
3561
  }
2808
3562
  return opts.mcp === false ? "agent-only" : "full";
2809
3563
  }
3564
+ var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
3565
+ async function printStarterPrompt(mode, cfg) {
3566
+ if (mode === "cli") {
3567
+ process.stdout.write(
3568
+ `
3569
+ ${style.bold("Next:")} pick up where you left off \u2014
3570
+ ${style.cyan("sechroom continuity resume-me")}
3571
+ `
3572
+ );
3573
+ return;
3574
+ }
3575
+ let primary = FALLBACK_AGENT_PROMPT;
3576
+ if (cfg) {
3577
+ try {
3578
+ const client = await makeClient(cfg);
3579
+ const { data } = await client.GET("/me/onboarding/starter-prompt", {});
3580
+ if (data?.primary) primary = data.primary;
3581
+ } catch {
3582
+ }
3583
+ }
3584
+ process.stdout.write(
3585
+ `
3586
+ ${style.bold("Next:")} paste this into your AI agent to get going \u2014
3587
+ ${style.cyan(`"${primary}"`)}
3588
+ `
3589
+ );
3590
+ }
3591
+
3592
+ // src/commands/sweep.ts
3593
+ import { existsSync as existsSync8 } from "fs";
3594
+ import { dirname as dirname6, join as join8, resolve as resolve2 } from "path";
3595
+ var DEFAULT_MANIFEST = join8(".sechroom", "repos.json");
3596
+ function planEntry(entry, root) {
3597
+ const dir = resolveChildDir(entry.path, root);
3598
+ if (!existsSync8(dir)) {
3599
+ return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
3600
+ }
3601
+ if (committedBindingPath(dir)) {
3602
+ return {
3603
+ label: entry.path,
3604
+ dir,
3605
+ disposition: "refresh",
3606
+ argv: ["onboard", "--refresh", "--yes"],
3607
+ reason: "bound (committed .sechroom.json) \u2014 refresh in place"
3608
+ };
3609
+ }
3610
+ if (entry.workspaceId) {
3611
+ return {
3612
+ label: entry.path,
3613
+ dir,
3614
+ disposition: "bind",
3615
+ argv: ["onboard", "--yes", "--local", "--workspace", entry.workspaceId],
3616
+ reason: `unbound \u2014 bind to ${entry.workspaceId} + commit .sechroom.json`
3617
+ };
3618
+ }
3619
+ return {
3620
+ label: entry.path,
3621
+ dir,
3622
+ disposition: "skip-unbound",
3623
+ argv: [],
3624
+ reason: "unbound + no workspaceId in the manifest \u2014 add one or run `sechroom onboard` there"
3625
+ };
3626
+ }
3627
+ function registerSweep(program2) {
3628
+ program2.command("sweep").description("Non-interactive fan-out from ./.sechroom/repos.json (headless sibling of `onboard --recurse`)").option("--manifest <path>", "path to the repos manifest", DEFAULT_MANIFEST).option("--dry-run", "print the plan (per-repo disposition + the onboard command) without running anything", false).addHelpText(
3629
+ "after",
3630
+ `
3631
+ For an interactive, no-manifest run use ${"`sechroom onboard --recurse`"} instead \u2014 it
3632
+ auto-discovers the child repos and prompts for a workspace per new one. ${"`sweep`"} is
3633
+ the deterministic manifest-driven form for scripts / CI.
3634
+
3635
+ Manifest \u2014 ./.sechroom/repos.json (per-operator, gitignored, alongside lane.json):
3636
+ {
3637
+ "repos": [
3638
+ { "path": "sechroom", "workspaceId": "wsp_XXXX" },
3639
+ { "path": "../other-repo", "workspaceId": "wsp_YYYY" },
3640
+ { "path": "already-bound" }
3641
+ ]
3642
+ }
3643
+
3644
+ Per repo (paths resolve relative to the manifest's root):
3645
+ ${ICON.refresh} bound committed .sechroom.json present \u2192 onboard --refresh (manifest workspace ignored)
3646
+ ${ICON.bind} unbound bind to the manifest workspaceId \u2192 onboard --local --workspace <id>
3647
+ ${ICON["skip-missing"]} missing directory does not exist \u2192 skipped
3648
+ ${ICON["skip-unbound"]} no workspace unbound + no workspaceId in manifest \u2192 skipped (add one, or onboard manually)
3649
+
3650
+ Examples:
3651
+ $ sechroom sweep --dry-run preview every repo's disposition, run nothing
3652
+ $ sechroom sweep onboard the whole tree from the root
3653
+ $ sechroom --tenant ocd sweep force a tenant for every child (else each resolves its own)`
3654
+ ).action((opts, cmd) => {
3655
+ const g = cmd.optsWithGlobals();
3656
+ const json = Boolean(g.json);
3657
+ const dryRun = Boolean(opts.dryRun);
3658
+ const manifestPath = resolve2(opts.manifest);
3659
+ let repos;
3660
+ try {
3661
+ repos = readManifest(manifestPath);
3662
+ } catch (err2) {
3663
+ fail(err2 instanceof Error ? err2.message : String(err2));
3664
+ }
3665
+ if (repos === null) {
3666
+ fail(`no manifest at ${manifestPath} \u2014 create ./.sechroom/repos.json, or use \`sechroom onboard --recurse\` to auto-discover (see \`sechroom sweep --help\`).`);
3667
+ }
3668
+ if (repos.length === 0) {
3669
+ if (json) process.stdout.write(JSON.stringify({ manifest: manifestPath, repos: [] }) + "\n");
3670
+ else process.stderr.write(`${warn("\u26A0")} ${manifestPath} lists no repos \u2014 nothing to do.
3671
+ `);
3672
+ return;
3673
+ }
3674
+ const root = dirname6(dirname6(manifestPath));
3675
+ const plans = repos.map((entry) => planEntry(entry, root));
3676
+ if (!json) {
3677
+ process.stderr.write(
3678
+ `${style.bold("sweep")} ${style.dim(`(${plans.length} repo${plans.length === 1 ? "" : "s"} from ${manifestPath})`)}
3679
+ `
3680
+ );
3681
+ }
3682
+ const results = runChildren(plans, { globals: passthroughGlobals(g), dryRun, json });
3683
+ if (json) {
3684
+ process.stdout.write(JSON.stringify({ manifest: manifestPath, dryRun, repos: results }) + "\n");
3685
+ return;
3686
+ }
3687
+ summarizeFanout(results, { dryRun });
3688
+ });
3689
+ }
2810
3690
 
2811
3691
  // src/commands/skills.ts
2812
- import { homedir as homedir4 } from "os";
2813
- import { dirname as dirname5, join as join5 } from "path";
2814
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, rmSync as rmSync2, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
3692
+ import { homedir as homedir6 } from "os";
3693
+ import { join as join9 } from "path";
3694
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, rmSync as rmSync2, existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
2815
3695
  var DEFAULT_SLUG = "operator-skills";
2816
3696
  var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
2817
3697
  var LOCK = ".sechroom-skills.json";
2818
3698
  function skillsDir(global) {
2819
- return global ? join5(homedir4(), ".claude", "skills") : join5(process.cwd(), ".claude", "skills");
3699
+ return global ? join9(homedir6(), ".claude", "skills") : join9(process.cwd(), ".claude", "skills");
2820
3700
  }
2821
3701
  function tagValue2(tags, prefix) {
2822
3702
  return (tags ?? []).find((t) => t.startsWith(prefix))?.slice(prefix.length);
@@ -2894,15 +3774,15 @@ Examples:
2894
3774
  const name = tagValue2(tags, "skill:");
2895
3775
  if (!name) continue;
2896
3776
  const body = m.text ?? m.Text ?? "";
2897
- mkdirSync4(join5(dir, name), { recursive: true });
2898
- writeFileSync4(join5(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
3777
+ mkdirSync6(join9(dir, name), { recursive: true });
3778
+ writeFileSync6(join9(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
2899
3779
  written.push(name);
2900
3780
  }
2901
- mkdirSync4(dir, { recursive: true });
2902
- const lockPath = join5(dir, LOCK);
2903
- const lock = existsSync5(lockPath) ? JSON.parse(readFileSync4(lockPath, "utf8")) : {};
3781
+ mkdirSync6(dir, { recursive: true });
3782
+ const lockPath = join9(dir, LOCK);
3783
+ const lock = existsSync9(lockPath) ? JSON.parse(readFileSync6(lockPath, "utf8")) : {};
2904
3784
  lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
2905
- writeFileSync4(lockPath, JSON.stringify(lock, null, 2) + "\n");
3785
+ writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
2906
3786
  if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
2907
3787
  const instanceNote = opts.instance ? ` (${opts.instance})` : "";
2908
3788
  console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
@@ -2924,42 +3804,41 @@ Examples:
2924
3804
  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) => {
2925
3805
  const slug = slugArg || DEFAULT_SLUG;
2926
3806
  const dir = skillsDir(!opts.local);
2927
- const lockPath = join5(dir, LOCK);
2928
- if (!existsSync5(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
2929
- const lock = JSON.parse(readFileSync4(lockPath, "utf8"));
3807
+ const lockPath = join9(dir, LOCK);
3808
+ if (!existsSync9(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
3809
+ const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
2930
3810
  const entry = lock[slug];
2931
3811
  if (!entry) fail(`No installed record for '${slug}' in ${lockPath}.`);
2932
3812
  const removed = [];
2933
3813
  for (const name of entry.skills) {
2934
- const skillPath = join5(dir, name);
2935
- if (existsSync5(skillPath)) {
3814
+ const skillPath = join9(dir, name);
3815
+ if (existsSync9(skillPath)) {
2936
3816
  rmSync2(skillPath, { recursive: true, force: true });
2937
3817
  removed.push(name);
2938
3818
  }
2939
3819
  }
2940
3820
  delete lock[slug];
2941
- writeFileSync4(lockPath, JSON.stringify(lock, null, 2) + "\n");
3821
+ writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
2942
3822
  if (opts.json) return emit({ slug, removed, dir }, true);
2943
3823
  console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
2944
3824
  });
2945
- skills.command("set-lane").description("Write this checkout's lane pin to a local ./.sem 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) => {
3825
+ 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) => {
2946
3826
  if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
2947
3827
  const target = localSemPath();
2948
- const values = readSem(target)?.values ?? {};
3828
+ const values = readLocalSemValues();
2949
3829
  if (opts.codeLane) values["code-lane"] = opts.codeLane;
2950
3830
  if (opts.designLane) values["design-lane"] = opts.designLane;
2951
- mkdirSync4(dirname5(target), { recursive: true });
2952
- writeFileSync4(target, serializeSem(values));
3831
+ writeSem(values, target);
2953
3832
  if (cmd.optsWithGlobals().json) return emit({ path: target, values }, true);
2954
- console.log(style.green(`Wrote lane pin \u2192 ${target}`));
3833
+ console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
2955
3834
  Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
2956
3835
  });
2957
- skills.command("lane").description("Show the lane pin resolved from ./.sem (nearest in this checkout)").option("--json", "machine output").action((opts, cmd) => {
3836
+ 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) => {
2958
3837
  const json = cmd.optsWithGlobals().json;
2959
3838
  const found = readSem();
2960
3839
  if (!found) {
2961
3840
  if (json) return emit({ path: null, values: {} }, true);
2962
- return console.log(style.dim(`No ./.sem pin in this checkout. Run 'sechroom skills set-lane'.`));
3841
+ return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
2963
3842
  }
2964
3843
  if (json) return emit(found, true);
2965
3844
  console.log(style.dim(`from ${found.path}`));
@@ -3028,22 +3907,22 @@ Examples:
3028
3907
  }
3029
3908
 
3030
3909
  // src/commands/reset.ts
3031
- import { homedir as homedir5 } from "os";
3032
- import { join as join6 } from "path";
3033
- import { existsSync as existsSync6, readFileSync as readFileSync5, rmSync as rmSync3 } from "fs";
3910
+ import { homedir as homedir7 } from "os";
3911
+ import { join as join10 } from "path";
3912
+ import { existsSync as existsSync10, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
3034
3913
  var SKILLS_LOCK = ".sechroom-skills.json";
3035
- var localSkillsDir = () => join6(process.cwd(), ".claude", "skills");
3036
- var globalSkillsDir = () => join6(homedir5(), ".claude", "skills");
3914
+ var localSkillsDir = () => join10(process.cwd(), ".claude", "skills");
3915
+ var globalSkillsDir = () => join10(homedir7(), ".claude", "skills");
3037
3916
  function removeMaterialisedSkills(dir) {
3038
3917
  const removed = [];
3039
- const lockPath = join6(dir, SKILLS_LOCK);
3040
- if (!existsSync6(lockPath)) return removed;
3918
+ const lockPath = join10(dir, SKILLS_LOCK);
3919
+ if (!existsSync10(lockPath)) return removed;
3041
3920
  try {
3042
- const lock = JSON.parse(readFileSync5(lockPath, "utf8"));
3921
+ const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
3043
3922
  for (const entry of Object.values(lock)) {
3044
3923
  for (const name of entry.skills ?? []) {
3045
- const p = join6(dir, name);
3046
- if (existsSync6(p)) {
3924
+ const p = join10(dir, name);
3925
+ if (existsSync10(p)) {
3047
3926
  rmSync3(p, { recursive: true, force: true });
3048
3927
  removed.push(p);
3049
3928
  }
@@ -3063,26 +3942,31 @@ function registerReset(program2) {
3063
3942
  removed ? style.green("Signed out \u2014 auth token removed.") : style.dim("Already signed out (no token).")
3064
3943
  );
3065
3944
  });
3066
- program2.command("reset").description("Reset LOCAL CLI state for this directory (./.sechroom.json, ./.sem, ./.claude/skills); --global also wipes the machine-wide token + config + ~/.claude/skills").option("--global", "also remove the global auth token, config, and ~/.claude/skills").option("-y, --yes", "don't prompt for confirmation").option("--json", "machine output").action(async (opts, cmd) => {
3945
+ program2.command("reset").description("Reset LOCAL CLI state for this directory (./.sechroom/, legacy ./.sechroom.json + ./.sem, ./.claude/skills); --global also wipes the machine-wide token + config + ~/.claude/skills").option("--global", "also remove the global auth token, config, and ~/.claude/skills").option("-y, --yes", "don't prompt for confirmation").option("--json", "machine output").action(async (opts, cmd) => {
3067
3946
  const json = cmd.optsWithGlobals().json;
3068
3947
  const global = Boolean(opts.global);
3069
3948
  if (!opts.yes && canPrompt()) {
3070
- const scope = global ? "this directory's local state AND your global auth token + config + ~/.claude/skills" : "this directory's local state (./.sechroom.json, ./.sem, ./.claude/skills)";
3949
+ const scope = global ? "this directory's local state AND your global auth token + config + ~/.claude/skills" : "this directory's local state (./.sechroom/, legacy ./.sechroom.json + ./.sem, ./.claude/skills)";
3071
3950
  if (!await promptYesNo(`Remove ${scope}?`)) {
3072
3951
  if (!json) console.log(style.dim("Cancelled."));
3073
3952
  return;
3074
3953
  }
3075
3954
  }
3076
3955
  const removed = [];
3077
- const localCfg = join6(process.cwd(), ".sechroom.json");
3078
- if (existsSync6(localCfg)) {
3079
- rmSync3(localCfg, { force: true });
3080
- removed.push(localCfg);
3956
+ const stateDir = join10(process.cwd(), ".sechroom");
3957
+ if (existsSync10(stateDir)) {
3958
+ rmSync3(stateDir, { recursive: true, force: true });
3959
+ removed.push(stateDir);
3960
+ }
3961
+ const legacyCfg = join10(process.cwd(), ".sechroom.json");
3962
+ if (existsSync10(legacyCfg)) {
3963
+ rmSync3(legacyCfg, { force: true });
3964
+ removed.push(legacyCfg);
3081
3965
  }
3082
- const sem = localSemPath();
3083
- if (existsSync6(sem)) {
3084
- rmSync3(sem, { force: true });
3085
- removed.push(sem);
3966
+ const legacySem = join10(process.cwd(), ".sem");
3967
+ if (existsSync10(legacySem)) {
3968
+ rmSync3(legacySem, { force: true });
3969
+ removed.push(legacySem);
3086
3970
  }
3087
3971
  removed.push(...removeMaterialisedSkills(localSkillsDir()));
3088
3972
  if (global) {
@@ -3107,7 +3991,7 @@ function registerReset(program2) {
3107
3991
  function resolveVersion() {
3108
3992
  try {
3109
3993
  const pkg = JSON.parse(
3110
- readFileSync6(new URL("../package.json", import.meta.url), "utf8")
3994
+ readFileSync8(new URL("../package.json", import.meta.url), "utf8")
3111
3995
  );
3112
3996
  return pkg.version ?? "0.0.0";
3113
3997
  } catch {
@@ -3123,7 +4007,7 @@ Examples:
3123
4007
  $ sechroom onboard guided first-run: configure, sign in, wire this project
3124
4008
  $ sechroom login sign in via browser (OAuth + PKCE)
3125
4009
  $ sechroom config set tenant ocd set your tenant (global)
3126
- $ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom.json)
4010
+ $ sechroom config set --local tenant cli-smoke pin tenant for this directory (committed .sechroom.json)
3127
4011
  $ sechroom config show resolved config + which source won
3128
4012
 
3129
4013
  $ sechroom memory create --text "a note" --title "Note" --tag idea
@@ -3135,7 +4019,7 @@ Examples:
3135
4019
  $ sechroom --json memory search "auth" compact JSON for scripts and agents
3136
4020
  $ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
3137
4021
 
3138
- Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom.json > global > default.
4022
+ Config precedence (high -> low): --flag > env (SECHROOM_*) > directory-local (committed ./.sechroom.json, shadowed per-field by the gitignored ./.sechroom/config.json override) > global > default.
3139
4023
  Run 'sechroom <command> --help' for command-specific examples.`
3140
4024
  );
3141
4025
  program.hook("preAction", (_thisCmd, actionCmd) => {
@@ -3161,11 +4045,11 @@ config.addHelpText(
3161
4045
  Examples:
3162
4046
  $ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
3163
4047
  $ sechroom config set tenant ocd
3164
- $ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom.json)
4048
+ $ sechroom config set --local tenant cli-smoke this dir + subdirs (committed .sechroom.json)
3165
4049
  $ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
3166
4050
  $ sechroom config show --json`
3167
4051
  );
3168
- config.command("set <key> <value>").description("Set baseUrl | tenant | workspaceId | defaultProjectId | clientId (clientId is global-only)").option("--local", "Write to the directory-local .sechroom.json (nearest up the tree, else cwd) instead of the global config").action((key, value, opts) => {
4052
+ config.command("set <key> <value>").description("Set baseUrl | tenant | workspaceId | defaultProjectId | clientId (clientId is global-only)").option("--local", "Write the committed directory-local .sechroom.json (nearest up the tree, else cwd) instead of the global config").action((key, value, opts) => {
3169
4053
  if (opts.local) {
3170
4054
  if (!["baseUrl", "tenant", "workspaceId", "defaultProjectId"].includes(key)) {
3171
4055
  process.stderr.write(`--local supports only: baseUrl | tenant | workspaceId | defaultProjectId (clientId is global)
@@ -3217,12 +4101,14 @@ registerWorkspace(program);
3217
4101
  registerProject(program);
3218
4102
  registerFiling(program);
3219
4103
  registerContinuity(program);
4104
+ registerHook(program);
3220
4105
  registerId(program);
3221
4106
  registerAccount(program);
3222
4107
  registerChat(program);
3223
4108
  registerInit(program);
3224
4109
  registerSetup(program);
3225
4110
  registerOnboard(program);
4111
+ registerSweep(program);
3226
4112
  registerSkills(program);
3227
4113
  registerReset(program);
3228
4114
  program.parseAsync().catch((err2) => {