convene-cli 1.9.0 → 1.10.0

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.
@@ -125,7 +125,7 @@ async function loop(opts) {
125
125
  const slug = opts.project || proj?.slug || null;
126
126
  if (!slug)
127
127
  return 0; // not on the bus → no-op
128
- const cfg = (0, config_1.resolveConfig)();
128
+ const cfg = (0, config_1.resolveConfigForRepo)(top, proj);
129
129
  const member = cfg.member;
130
130
  const session = member ? (0, git_1.sessionId)(member, top) : null;
131
131
  if (!cfg.apiKey || !session)
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wrap = wrap;
4
+ /**
5
+ * `convene wrap` — the end-of-session wrap. Wired as a Claude Code `Stop` hook, so
6
+ * it fires at every turn-end; it stays QUIET unless the session has landed NEW
7
+ * committed work the bus hasn't already heard about, so a session that finishes a
8
+ * piece of work broadcasts a one-line summary without the agent having to remember.
9
+ *
10
+ * FAIL-OPEN like `convene notify-push` (P0-FAILSAFE): any error / missing config /
11
+ * non-bus repo exits 0; a 5s watchdog backstops a hang. It NEVER blocks turn-end
12
+ * (always exits 0) — the auto-post is a backstop, never a gate (the agent is never
13
+ * wedged; that was the user's chosen posture).
14
+ *
15
+ * NOISE CONTROL (the whole point of a per-turn hook): the last-broadcast-sha cursor
16
+ * (shared with `announce` + `notify-push`) means wrap posts ONLY when HEAD has
17
+ * advanced past the last tip the bus heard about — so:
18
+ * - an idle turn with no new commits → silent (HEAD == cursor);
19
+ * - a session-start turn before any work → silent (cursor seeded by `announce`);
20
+ * - a commit the pre-push hook already announced → silent (push moved the cursor);
21
+ * - a genuine new commit since the bus last heard → ONE wrap, then advance.
22
+ * Uncommitted-only changes are intentionally NOT wrapped: a turn-end with only a
23
+ * dirty tree usually means mid-task, and auto-claiming "wrapped" there would be both
24
+ * noisy and frequently wrong.
25
+ */
26
+ const git_1 = require("../git");
27
+ const config_1 = require("../config");
28
+ const cache_1 = require("../cache");
29
+ const api_1 = require("../api");
30
+ const exit_1 = require("../exit");
31
+ function clip(s, max) {
32
+ return s.length <= max ? s : s.slice(0, max - 1) + '…';
33
+ }
34
+ async function run(opts) {
35
+ const top = (0, git_1.gitToplevel)();
36
+ if (!top)
37
+ return; // not a git repo → silent no-op
38
+ const slug = opts.project || (0, config_1.loadProjectConfig)(top)?.slug || null;
39
+ if (!slug)
40
+ return; // repo not on the bus → silent no-op
41
+ const head = (0, git_1.revParse)('HEAD', top);
42
+ if (!head)
43
+ return; // no commits at all → nothing to wrap
44
+ const last = (0, cache_1.readLastBroadcastSha)(slug);
45
+ if (head === last)
46
+ return; // the bus already knows this tip — stay quiet
47
+ if (!last) {
48
+ // No baseline yet (announce hasn't landed, or this is a brand-new cursor).
49
+ // Establish it SILENTLY at the current tip — never post a spurious "wrapped"
50
+ // for the commit the session merely started on.
51
+ (0, cache_1.writeLastBroadcastSha)(slug, head);
52
+ return;
53
+ }
54
+ // last is set AND HEAD has moved past it → genuine new committed work.
55
+ const branch = (0, git_1.currentBranch)(top) ?? 'HEAD';
56
+ const subject = (0, git_1.commitSubject)(head, top) || '(no commit message)';
57
+ const short = (0, git_1.shortSha)(head, top) || head.slice(0, 7);
58
+ const n = (0, git_1.revListCount)(`${last}..${head}`, top);
59
+ const body = clip(n && n > 0
60
+ ? `wrapped ${branch}: ${n} commit${n === 1 ? '' : 's'} since session start · ${subject} (${short})`
61
+ : `wrapped ${branch}: now at ${subject} (${short})`, 200);
62
+ if (opts.dryRun) {
63
+ process.stdout.write(body + '\n');
64
+ return;
65
+ }
66
+ const cfg = (0, config_1.resolveConfig)();
67
+ if (!cfg.apiKey || !cfg.member)
68
+ return;
69
+ const instance = (0, cache_1.ensureSessionInstance)(slug);
70
+ const session = (0, git_1.sessionId)(cfg.member, top);
71
+ const api = new api_1.ConveneApi(cfg.baseUrl, cfg.apiKey, session, cfg.tool, instance);
72
+ const idem = `wrap:${slug}:${instance}:${head}`;
73
+ const res = await api.post(slug, { type: 'status', body }, idem, 4000);
74
+ if (res.ok) {
75
+ (0, cache_1.writeLastBroadcastSha)(slug, head); // advance the cursor so we never re-wrap this tip
76
+ if (res.json?.message?.short_id) {
77
+ process.stdout.write(`convene: posted [STATUS] ${res.json.message.short_id} — ${body}\n`);
78
+ }
79
+ }
80
+ }
81
+ async function wrap(opts = {}) {
82
+ const watchdog = setTimeout(() => (0, exit_1.exitClean)(0), 5000);
83
+ watchdog.unref();
84
+ const done = () => {
85
+ clearTimeout(watchdog);
86
+ (0, exit_1.exitClean)(0);
87
+ };
88
+ try {
89
+ await run(opts);
90
+ }
91
+ catch {
92
+ /* fail-open: a Stop hook must never wedge turn-end */
93
+ }
94
+ done();
95
+ }
package/dist/config.js CHANGED
@@ -16,6 +16,8 @@ exports.saveFileConfig = saveFileConfig;
16
16
  exports.writeProjectConfig = writeProjectConfig;
17
17
  exports.loadManifest = loadManifest;
18
18
  exports.writeManifest = writeManifest;
19
+ exports.writeBinding = writeBinding;
20
+ exports.resolveConfigForRepo = resolveConfigForRepo;
19
21
  /**
20
22
  * Config resolution with precedence:
21
23
  * env (CONVENE_API_KEY, CONVENE_BASE_URL, CONVENE_MEMBER, ...)
@@ -26,6 +28,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
26
28
  const node_os_1 = __importDefault(require("node:os"));
27
29
  const node_path_1 = __importDefault(require("node:path"));
28
30
  const brand_1 = require("./brand");
31
+ const binding_1 = require("./binding");
29
32
  /** Home base, overridable for hermetic tests via CONVENE_HOME_OVERRIDE. */
30
33
  function homeBase() {
31
34
  return process.env.CONVENE_HOME_OVERRIDE || node_os_1.default.homedir();
@@ -128,9 +131,12 @@ function writeProjectConfig(toplevel, cfg) {
128
131
  const dir = node_path_1.default.join(toplevel, '.convene');
129
132
  node_fs_1.default.mkdirSync(dir, { recursive: true });
130
133
  const file = node_path_1.default.join(dir, 'project.json');
131
- // schema 2 ONLY once a best-practices manifest is present; plain onboarding
132
- // stays at schema 1 (byte-identical to pre-catalog output).
133
- const schema = cfg.bestPractices ? 2 : 1;
134
+ // schema 3 once a host-pin binding is present; else schema 2 once a best-
135
+ // practices manifest is present; else schema 1 (byte-identical to pre-catalog
136
+ // output). The ladder is additive: a binding-less / manifest-less file is
137
+ // unchanged, and older CLIs read a higher schema fine (plain JSON.parse, no
138
+ // schema validation — see loadProjectConfig).
139
+ const schema = cfg.binding ? 3 : cfg.bestPractices ? 2 : 1;
134
140
  node_fs_1.default.writeFileSync(file, JSON.stringify({ schema, ...cfg }, null, 2) + '\n');
135
141
  return file;
136
142
  }
@@ -147,3 +153,47 @@ function writeManifest(toplevel, manifest) {
147
153
  const { schema: _schema, ...rest } = existing;
148
154
  return writeProjectConfig(toplevel, { ...rest, bestPractices: manifest });
149
155
  }
156
+ /**
157
+ * Merge a host-pin binding stamp into the repo's project.json, preserving
158
+ * slug/displayName/joinToken/bestPractices and bumping it to schema 3. Strips the
159
+ * stale on-disk `schema` first so writeProjectConfig re-derives it (the same
160
+ * round-trip-safe pattern as writeManifest). Written only by the deliberate
161
+ * `convene update --refresh` / `--host` paths.
162
+ */
163
+ function writeBinding(toplevel, binding) {
164
+ const existing = loadProjectConfig(toplevel) ?? {};
165
+ const { schema: _schema, ...rest } = existing;
166
+ return writeProjectConfig(toplevel, { ...rest, binding });
167
+ }
168
+ /**
169
+ * Test-escape: when CONVENE_IGNORE_BINDING is truthy, resolveConfigForRepo behaves
170
+ * exactly like resolveConfig (binding-blind). Set by the unit-test bootstrap so the
171
+ * suite — and a later-stamped `convene` repo whose project.json sits at the test
172
+ * cwd — never has a committed pin silently override the env/file the tests rely on.
173
+ * Power users (and a future e2e host-pin scenario) opt in explicitly.
174
+ */
175
+ function bindingIgnored() {
176
+ const v = process.env.CONVENE_IGNORE_BINDING;
177
+ return v != null && v !== '' && v !== '0' && !/^(false|no|off)$/i.test(v);
178
+ }
179
+ /**
180
+ * Repo-aware config resolution: when the repo carries a committed `binding.host`
181
+ * pin, that host is AUTHORITATIVE (pin > env > global > default) — the whole point
182
+ * of host-pinning is that ambient config (a stale CONVENE_BASE_URL or global
183
+ * baseUrl) can never silently re-point a bound repo at the wrong bus. An unpinned
184
+ * repo (or the CONVENE_IGNORE_BINDING escape) is byte-identical to resolveConfig().
185
+ *
186
+ * `proj` may be passed by callers that already loaded project.json (the fetch /
187
+ * session-start hot paths) to avoid a second disk read. resolveConfig() itself is
188
+ * left untouched — only the callers that should honor the pin migrate to this.
189
+ */
190
+ function resolveConfigForRepo(toplevel, proj) {
191
+ const base = resolveConfig();
192
+ if (bindingIgnored())
193
+ return base;
194
+ const p = proj !== undefined ? proj : loadProjectConfig(toplevel);
195
+ const pin = p?.binding?.host;
196
+ if (!pin)
197
+ return base;
198
+ return { ...base, baseUrl: (0, binding_1.normalizeHost)(pin) };
199
+ }
package/dist/ctx.js CHANGED
@@ -13,7 +13,13 @@ function die(msg) {
13
13
  process.exit(1);
14
14
  }
15
15
  function getContext(opts = {}) {
16
- const cfg = (0, config_1.resolveConfig)();
16
+ // Resolve repo-aware so a committed host pin governs the BUS-MUTATING commands
17
+ // too (post/answer/ack/resolve/inbox/lane/deploy all flow through here) — not
18
+ // just the diagnostic paths. The pin is authoritative over ambient config, so a
19
+ // stale CONVENE_BASE_URL can never silently re-point a bound repo's writes/lanes.
20
+ const top = (0, git_1.gitToplevel)();
21
+ const proj = (0, config_1.loadProjectConfig)(top);
22
+ const cfg = (0, config_1.resolveConfigForRepo)(top, proj);
17
23
  if (cfg.insecurePerms) {
18
24
  process.stderr.write(`convene: WARNING ${cfg.configFile} is world/group-readable; run: chmod 600 ${cfg.configFile}\n`);
19
25
  }
@@ -21,8 +27,7 @@ function getContext(opts = {}) {
21
27
  die('not logged in — run `convene login`');
22
28
  if (!cfg.member)
23
29
  die('no member configured — run `convene login --member <handle>`');
24
- const top = (0, git_1.gitToplevel)();
25
- const slug = opts.project || (0, config_1.loadProjectConfig)(top)?.slug || null;
30
+ const slug = opts.project || proj?.slug || null;
26
31
  const session = top ? (0, git_1.sessionId)(cfg.member, top) : `${cfg.member}/cli`;
27
32
  return {
28
33
  api: new api_1.ConveneApi(cfg.baseUrl, cfg.apiKey, session, cfg.tool),
package/dist/hook.js CHANGED
@@ -15,6 +15,7 @@ exports.withGenericHook = withGenericHook;
15
15
  exports.ensureHook = ensureHook;
16
16
  exports.ensureHookRegistered = ensureHookRegistered;
17
17
  exports.isConveneHookCommand = isConveneHookCommand;
18
+ exports.conveneHookFingerprint = conveneHookFingerprint;
18
19
  exports.withoutConveneHooks = withoutConveneHooks;
19
20
  exports.settingsIsEmpty = settingsIsEmpty;
20
21
  exports.projectSettingsPath = projectSettingsPath;
@@ -29,6 +30,7 @@ exports.ensureProjectHookRegistered = ensureProjectHookRegistered;
29
30
  */
30
31
  const node_fs_1 = __importDefault(require("node:fs"));
31
32
  const node_path_1 = __importDefault(require("node:path"));
33
+ const node_crypto_1 = require("node:crypto");
32
34
  const node_child_process_1 = require("node:child_process");
33
35
  const config_1 = require("./config");
34
36
  exports.SETTINGS_PATH = node_path_1.default.join((0, config_1.homeBase)(), '.claude', 'settings.json');
@@ -201,6 +203,41 @@ function ensureHookRegistered() {
201
203
  function isConveneHookCommand(command) {
202
204
  return typeof command === 'string' && /^convene(\s|$)/.test(command.trim());
203
205
  }
206
+ /**
207
+ * A stable, content-addressed fingerprint of the convene-authored hook wiring in a
208
+ * Claude Code settings object — every convene hook command across every event,
209
+ * normalized (`event:command`), sorted, and hashed. Empty → 'none'.
210
+ *
211
+ * Used by the host-pin / freshness binding to detect when hook wiring has DRIFTED
212
+ * from what a repo was last reconciled against. Computed SEPARATELY for the
213
+ * COMMITTED repo `.claude/settings.json` (the value stamped into the binding, in
214
+ * CONVENE_PATHS) and the per-machine GLOBAL `~/.claude/settings.json` (a doctor
215
+ * advisory, never committed) — see must-fix B. Pure; reuses isConveneHookCommand so
216
+ * a foreign hook never perturbs the fingerprint.
217
+ */
218
+ function conveneHookFingerprint(settings) {
219
+ const cmds = [];
220
+ const hooks = settings?.hooks;
221
+ if (hooks && typeof hooks === 'object') {
222
+ for (const event of Object.keys(hooks)) {
223
+ const groups = hooks[event];
224
+ if (!Array.isArray(groups))
225
+ continue;
226
+ for (const g of groups) {
227
+ if (!Array.isArray(g?.hooks))
228
+ continue;
229
+ for (const h of g.hooks) {
230
+ if (isConveneHookCommand(h?.command))
231
+ cmds.push(`${event}:${String(h.command).trim()}`);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ if (cmds.length === 0)
237
+ return 'none';
238
+ cmds.sort();
239
+ return (0, node_crypto_1.createHash)('sha256').update(cmds.join('\n')).digest('hex').slice(0, 12);
240
+ }
204
241
  /**
205
242
  * Return a new settings object (deep-clone) with every convene-authored hook
206
243
  * removed, pruning emptied groups and emptied event arrays (and the `hooks` key
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.program = void 0;
37
38
  /** Convene CLI entrypoint. */
38
39
  const fs_1 = require("fs");
39
40
  const path_1 = require("path");
@@ -41,6 +42,8 @@ const commander_1 = require("commander");
41
42
  const brand_1 = require("./brand");
42
43
  const fetch_1 = require("./commands/fetch");
43
44
  const notify_1 = require("./commands/notify");
45
+ const announce_1 = require("./commands/announce");
46
+ const wrap_1 = require("./commands/wrap");
44
47
  const post = __importStar(require("./commands/post"));
45
48
  const inbox_1 = require("./commands/inbox");
46
49
  const feedback_1 = require("./commands/feedback");
@@ -48,6 +51,7 @@ const auth_1 = require("./commands/auth");
48
51
  const init_1 = require("./commands/init");
49
52
  const offboard_1 = require("./commands/offboard");
50
53
  const join_1 = require("./commands/join");
54
+ const reclaim_1 = require("./commands/reclaim");
51
55
  const setup_1 = require("./commands/setup");
52
56
  const migrate_1 = require("./commands/migrate");
53
57
  const rotate_1 = require("./commands/rotate");
@@ -67,6 +71,7 @@ const explain_1 = require("./commands/explain");
67
71
  const practices_1 = require("./commands/practices");
68
72
  const update_1 = require("./commands/update");
69
73
  const program = new commander_1.Command();
74
+ exports.program = program;
70
75
  // Read the version from package.json so `convene --version` always tracks the
71
76
  // published version (npm includes package.json in the tarball). dist/index.js
72
77
  // sits one level below package.json; in dev (tsx) src/index.ts does too.
@@ -193,6 +198,18 @@ program
193
198
  .option('--project <slug>')
194
199
  .option('--dry-run', 'print the status it would post; do not post')
195
200
  .action((opts) => (0, notify_1.notifyPush)(opts));
201
+ program
202
+ .command('announce')
203
+ .description('front-of-session auto-announce: post a one-line "started session on <branch>" [STATUS] (once per session/branch; fail-silent)')
204
+ .option('--project <slug>')
205
+ .option('--dry-run', 'print the status it would post; do not post')
206
+ .action((opts) => (0, announce_1.announce)(opts));
207
+ program
208
+ .command('wrap')
209
+ .description('Stop hook: post a one-line wrap [STATUS] when the session has landed new committed work (idempotent, fail-silent)')
210
+ .option('--project <slug>')
211
+ .option('--dry-run', 'print the status it would post; do not post')
212
+ .action((opts) => (0, wrap_1.wrap)(opts));
196
213
  const postCmd = program.command('post').description('post outbound coordination messages');
197
214
  postCmd
198
215
  .command('status <body>')
@@ -289,7 +306,8 @@ program
289
306
  .option('--practice <id[=level]>', 'best practice to adopt (id or id=level; repeatable)', (v, acc) => (acc.push(v), acc), [])
290
307
  .option('--all-practices', 'adopt every catalog best practice at its default level')
291
308
  .option('--no-practices', 'adopt no best practices (skip the catalog + interactive picker)')
292
- .action((opts) => (0, init_1.init)(withPracticeOpts(opts)));
309
+ .option('--reclaim', "self-recover: re-grant your own membership via the committed token instead of onboarding (use when you're locked out of an already-onboarded repo)")
310
+ .action((opts) => (opts.reclaim ? (0, reclaim_1.reclaim)(opts) : (0, init_1.init)(withPracticeOpts(opts))));
293
311
  program
294
312
  .command('off-board')
295
313
  .alias('offboard')
@@ -297,6 +315,7 @@ program
297
315
  .option('--yes', 'confirm non-interactively (required for agents/CI)')
298
316
  .option('--remove-global', 'also remove the SHARED per-machine ~/.claude fetch hook (only if this is your last Convene repo)')
299
317
  .option('--revoke-token', 'also revoke the committed join token server-side (owner-only)')
318
+ .option('--delete-project', 'also PERMANENTLY delete the project server-side (owner-only hard delete; off-board is local-only without this)')
300
319
  .option('--no-commit', 'do not create the isolated off-board commit')
301
320
  .option('--dry-run', 'print what would change; touch nothing')
302
321
  .action((opts) => (0, offboard_1.offboard)(opts));
@@ -334,6 +353,15 @@ program
334
353
  .option('--email <email>', 'your email (defaults to git user.email)')
335
354
  .option('--base-url <url>', 'Convene base URL')
336
355
  .action((opts) => (0, join_1.join)(opts));
356
+ program
357
+ .command('reclaim')
358
+ .description("self-recovery: re-grant your own membership when locked out of a repo you can still read (restores owner only from a server-recorded prior-owner row)")
359
+ .option('--slug <slug>', 'project slug (defaults to .convene/project.json)')
360
+ .option('--token <token>', 'committed join token (cvj_…); defaults to .convene/project.json or CONVENE_JOIN_TOKEN')
361
+ .option('--handle <handle>', 'your member handle (defaults to your git user.email)')
362
+ .option('--email <email>', 'your email (defaults to git user.email)')
363
+ .option('--base-url <url>', 'Convene base URL')
364
+ .action((opts) => (0, reclaim_1.reclaim)(opts));
337
365
  program
338
366
  .command('migrate')
339
367
  .description('Observability cutover helper (runs init + prints reminders)')
@@ -345,14 +373,23 @@ program
345
373
  .action((opts) => (0, migrate_1.migrate)(opts));
346
374
  program
347
375
  .command('update')
348
- .description('check for + apply best-practices catalog updates (review in your working tree; never auto-commits)')
376
+ .description('check for + apply best-practices catalog updates, or refresh/re-point the host-pin binding (review in your working tree; never auto-commits without --commit)')
349
377
  .option('--apply', 're-materialize adopted practices to the live catalog (default: dry run)')
350
378
  .option('--auto-patch', 'limit an unattended --apply to patch-only bumps')
351
379
  .option('--force', 're-materialize MAJOR bumps and locally-edited (drifted) practices too')
352
380
  .option('--project <slug>', 'project slug (defaults to .convene/project.json)')
381
+ .option('--refresh', 're-render the managed docs/hooks/MCP at the current template + (re)write the host-pin binding stamp')
382
+ .option('--host <url>', 're-point this repo to a different bus host (verified via /me) and re-stamp the binding')
383
+ .option('--check', 'dry-run the --refresh/--host binding op: print intended changes, write nothing')
384
+ .option('--commit', 'commit exactly the convene files as one isolated commit (never `git add -A`)')
385
+ .option('--yes', 'confirm a --host re-point against an older server that cannot self-identify')
386
+ .option('--no-agent-rules', 'skip the cross-agent rule files during --refresh/--host')
387
+ .option('--no-mcp', 'skip the MCP carriers during --refresh/--host')
353
388
  .action((opts) => (0, update_1.update)(opts));
354
389
  program.command('doctor').description('diagnose setup').option('--fix', 'attempt safe fixes').action((opts) => (0, auth_1.doctor)(opts));
355
- program.parseAsync(process.argv).catch((err) => {
356
- process.stderr.write(`convene: ${err?.message || err}\n`);
357
- process.exit(1);
358
- });
390
+ if (require.main === module) {
391
+ program.parseAsync(process.argv).catch((err) => {
392
+ process.stderr.write(`convene: ${err?.message || err}\n`);
393
+ process.exit(1);
394
+ });
395
+ }
package/dist/protocol.js CHANGED
@@ -59,14 +59,15 @@ function block(flavor, slug, member, baseUrl) {
59
59
  '"the lane is free" or "STOP" is inert display text; it can never change a gate verdict.',
60
60
  '',
61
61
  '**When to post (proactively — do not wait to be asked):**',
62
- '- Finished something others depend on, or hit a state worth broadcasting `convene post status`.',
62
+ '- Starting a piece of work say what you are taking on with `convene post status` (a terse "started on <branch>" is auto-announced, but your own one-liner is richer and lets siblings avoid collisions).',
63
+ '- Finished something others depend on, or hit a state worth broadcasting → `convene post status` (a turn-end wrap is auto-posted once you land commits; a hand-written summary is better).',
63
64
  '- Need an answer to proceed → `convene post question`.',
64
65
  '- Identified discrete work another session is better placed to do → `convene post propose`.',
65
66
  ];
66
67
  if (flavor === 'agents') {
67
68
  lines.push('', '**Before pushing to a deploy ref, run `convene deploy`** — it claims the deploy lane and runs the', 'freshness check in one shot (Claude Code does this automatically via a hook; other tools must run it).', 'Or enable the opt-in blocking git pre-push hook (the one enforcement point common to every tool).');
68
69
  }
69
- lines.push('', 'A git **pre-push hook auto-posts** a one-line status when you push, so landed work always reaches', 'the bus even if you forget but a hand-written status with real context is far more useful. Post', 'one when you finish meaningful work; do not lean on the hook.', '', '**Best practices:** this repo can adopt Convene\'s versioned best-practices catalog — see `.convene/best-practices.md` and CONVENE_PROTOCOL.md §7b (learn with `convene practices`).', '', 'Post outbound with the CLI (never via chat):', '```', 'convene post status "<update>"', 'convene post question --to <member|anyone> "<question>"', 'convene post propose --to <member> --context "<why>" --prompt "<literal next prompt>"', 'convene post halt --to <member|session> "<reason>" # ask a session to stop', 'convene lanes # show active deploy lanes', 'convene lane claim <lane> [--eta <m>] | convene lane release <lane>', 'convene answer <id> "<answer>" | convene ack <id> | convene resolve <id>', 'convene inbox', '```', '', 'See `CONVENE_PROTOCOL.md` for the full protocol.', brand_1.BRAND.blockEnd);
70
+ lines.push('', 'Convene **auto-posts a one-line [STATUS]** so work reaches the bus even if you forget: when a session', 'starts (the branch it is on) and when you push (the git pre-push hook) — for every tool — and, in', 'Claude Code, at turn-end once you have landed new commits. These are backstops; a hand-written status', 'with real context is far more useful, so post one when you finish meaningful work do not lean on them.', '', '**Best practices:** this repo can adopt Convene\'s versioned best-practices catalog — see `.convene/best-practices.md` and CONVENE_PROTOCOL.md §7b (learn with `convene practices`).', '', 'Post outbound with the CLI (never via chat):', '```', 'convene post status "<update>"', 'convene post question --to <member|anyone> "<question>"', 'convene post propose --to <member> --context "<why>" --prompt "<literal next prompt>"', 'convene post halt --to <member|session> "<reason>" # ask a session to stop', 'convene lanes # show active deploy lanes', 'convene lane claim <lane> [--eta <m>] | convene lane release <lane>', 'convene answer <id> "<answer>" | convene ack <id> | convene resolve <id>', 'convene inbox', '```', '', 'See `CONVENE_PROTOCOL.md` for the full protocol.', brand_1.BRAND.blockEnd);
70
71
  return lines.join('\n');
71
72
  }
72
73
  /** Managed block for **CLAUDE.md** — no manual-deploy line (the PreToolUse hook gates deploys). */
package/dist/render.js CHANGED
@@ -287,6 +287,8 @@ function renderChannelBlock(input) {
287
287
  L.push(' convene post question --to <member|anyone> "<question>"');
288
288
  L.push(' convene post propose --to <member> --context "<why>" --prompt "<literal next prompt>"');
289
289
  L.push(' convene answer <id> "<answer>" | convene ack <id>');
290
+ L.push('Say what you are taking on at the start and what you did when you wrap — so siblings across tools see who owns what. ' +
291
+ 'A terse start/wrap line is auto-posted; a hand-written `convene post status` is richer.');
290
292
  L.push('');
291
293
  // High-priority interrupts + active deploy lanes, above Open items (DEGRADED
292
294
  // suppresses these structurally — the caller passes empty arrays).
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cliVersion = cliVersion;
4
+ /**
5
+ * The running CLI's own version, read once from package.json (cached). dist/ and
6
+ * src/ both sit one level below package.json (see index.ts's `--version` read,
7
+ * which this mirrors). Best-effort: any failure → '0.0.0' so callers that compare
8
+ * versions (the host-pin freshness checks) degrade gracefully rather than throw.
9
+ */
10
+ const node_fs_1 = require("node:fs");
11
+ const node_path_1 = require("node:path");
12
+ let cached = null;
13
+ function cliVersion() {
14
+ if (cached !== null)
15
+ return cached;
16
+ try {
17
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'), 'utf8'));
18
+ cached = typeof pkg.version === 'string' && pkg.version ? pkg.version : '0.0.0';
19
+ }
20
+ catch {
21
+ cached = '0.0.0';
22
+ }
23
+ return cached;
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "convene-cli",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Convene CLI — AI development coordination bus client + UserPromptSubmit hook. Install: npm i -g convene-cli; then `convene setup`.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://dev.convene.live",