convene-cli 1.5.1 → 1.7.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.
- package/dist/api.js +9 -0
- package/dist/cache.js +173 -1
- package/dist/catalog/prompt.js +27 -5
- package/dist/commands/auth.js +128 -3
- package/dist/commands/beat.js +145 -0
- package/dist/commands/catchup.js +3 -1
- package/dist/commands/fetch.js +20 -3
- package/dist/commands/init.js +11 -1
- package/dist/commands/session-start.js +77 -7
- package/dist/commands/watch-reap.js +212 -0
- package/dist/commands/watch.js +109 -26
- package/dist/commands/worktree.js +155 -17
- package/dist/config.js +30 -0
- package/dist/index.js +12 -0
- package/dist/render.js +45 -1
- package/package.json +1 -1
|
@@ -3,7 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.copyWorktreeIncludes = copyWorktreeIncludes;
|
|
7
|
+
exports.provisionWorktree = provisionWorktree;
|
|
6
8
|
exports.worktree = worktree;
|
|
9
|
+
exports.provisionAutoWorktree = provisionAutoWorktree;
|
|
7
10
|
/**
|
|
8
11
|
* `convene worktree <branch>` — create an isolated git worktree for a parallel
|
|
9
12
|
* session. This is Convene's recommended default for running several coding agents
|
|
@@ -14,6 +17,10 @@ exports.worktree = worktree;
|
|
|
14
17
|
*
|
|
15
18
|
* DIE-LOUD like the other interactive verbs (stderr + non-zero exit on failure).
|
|
16
19
|
* Pure git plumbing — does NOT require the repo to be on the Convene bus.
|
|
20
|
+
*
|
|
21
|
+
* The provisioning CORE (`provisionWorktree`) is factored out of the command so
|
|
22
|
+
* the auto-isolate SessionStart path (`provisionAutoWorktree`) can reuse the exact
|
|
23
|
+
* same git plumbing + `.worktreeinclude` copy — fail-OPEN there, die-LOUD here.
|
|
17
24
|
*/
|
|
18
25
|
const node_child_process_1 = require("node:child_process");
|
|
19
26
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -24,6 +31,88 @@ function refExists(ref, cwd) {
|
|
|
24
31
|
const r = (0, node_child_process_1.spawnSync)('git', ['rev-parse', '--verify', '--quiet', ref], { cwd, encoding: 'utf8' });
|
|
25
32
|
return r.status === 0;
|
|
26
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The `.worktreeinclude` format (read from `<top>/.worktreeinclude`):
|
|
36
|
+
* - one relative path per line, resolved against the SOURCE worktree's toplevel;
|
|
37
|
+
* - lines beginning with `#` are comments; blank/whitespace-only lines are ignored;
|
|
38
|
+
* - a leading `!` or absolute path or `..` traversal is rejected (never escapes <top>);
|
|
39
|
+
* - each entry may be a file OR a directory (copied recursively);
|
|
40
|
+
* - a missing entry is silently skipped (gitignored config that just isn't present).
|
|
41
|
+
*
|
|
42
|
+
* Purpose: carry gitignored local config (e.g. `.env`, `.envrc`, `.claude/settings.local.json`)
|
|
43
|
+
* from the source checkout into a freshly-provisioned worktree, since git itself
|
|
44
|
+
* only materializes TRACKED files. Best-effort: any per-entry error is swallowed so
|
|
45
|
+
* a copy failure can never wedge worktree creation (critical on the auto-isolate path).
|
|
46
|
+
*/
|
|
47
|
+
function copyWorktreeIncludes(top, dest) {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = node_fs_1.default.readFileSync(node_path_1.default.join(top, '.worktreeinclude'), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return; // no include file → nothing to carry
|
|
54
|
+
}
|
|
55
|
+
for (const line of raw.split('\n')) {
|
|
56
|
+
const entry = line.trim();
|
|
57
|
+
if (!entry || entry.startsWith('#'))
|
|
58
|
+
continue;
|
|
59
|
+
// Reject anything that could escape <top>: leading '!', absolute, or any '..' segment.
|
|
60
|
+
if (entry.startsWith('!') || node_path_1.default.isAbsolute(entry))
|
|
61
|
+
continue;
|
|
62
|
+
const rel = node_path_1.default.normalize(entry);
|
|
63
|
+
if (rel === '..' || rel.startsWith('..' + node_path_1.default.sep) || rel.split(node_path_1.default.sep).includes('..'))
|
|
64
|
+
continue;
|
|
65
|
+
const src = node_path_1.default.join(top, rel);
|
|
66
|
+
const dst = node_path_1.default.join(dest, rel);
|
|
67
|
+
try {
|
|
68
|
+
if (!node_fs_1.default.existsSync(src))
|
|
69
|
+
continue; // skip missing entries
|
|
70
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(dst), { recursive: true });
|
|
71
|
+
node_fs_1.default.cpSync(src, dst, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* best-effort per entry — never let a copy failure wedge provisioning */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Hard ceiling on `git worktree add` (a full working-tree checkout). On the AUTO
|
|
80
|
+
* path this runs SYNCHRONOUSLY inside SessionStart, and the boot's async 6s
|
|
81
|
+
* watchdog cannot interrupt synchronous work — so a large/slow repo could stall the
|
|
82
|
+
* boot. A bounded spawnSync caps that: a timeout kills git and we fail-open.
|
|
83
|
+
*/
|
|
84
|
+
const WORKTREE_ADD_TIMEOUT_MS = 20_000;
|
|
85
|
+
/**
|
|
86
|
+
* The reusable provisioning core: resolve the branch (existing local / existing
|
|
87
|
+
* remote-only / new), run `git worktree add`, then copy `.worktreeinclude` config
|
|
88
|
+
* into the new tree. Returns {dest, branch} on success or null on ANY failure —
|
|
89
|
+
* the caller decides whether to die-loud (interactive) or fail-open (auto).
|
|
90
|
+
*/
|
|
91
|
+
function provisionWorktree(args) {
|
|
92
|
+
const { top, branch, dest, fromRef, quiet } = args;
|
|
93
|
+
if (node_fs_1.default.existsSync(dest))
|
|
94
|
+
return null;
|
|
95
|
+
const localExists = refExists(`refs/heads/${branch}`, top);
|
|
96
|
+
const remoteExists = !localExists && refExists(`refs/remotes/origin/${branch}`, top);
|
|
97
|
+
// Existing local branch → check it out; existing remote-only branch → create a
|
|
98
|
+
// local tracking branch; otherwise → new branch from fromRef (or HEAD).
|
|
99
|
+
const gitArgs = localExists
|
|
100
|
+
? ['worktree', 'add', dest, branch]
|
|
101
|
+
: remoteExists
|
|
102
|
+
? ['worktree', 'add', '-b', branch, dest, `origin/${branch}`]
|
|
103
|
+
: ['worktree', 'add', '-b', branch, dest, fromRef || 'HEAD'];
|
|
104
|
+
const r = (0, node_child_process_1.spawnSync)('git', gitArgs, {
|
|
105
|
+
cwd: top,
|
|
106
|
+
stdio: quiet ? 'ignore' : 'inherit',
|
|
107
|
+
timeout: WORKTREE_ADD_TIMEOUT_MS,
|
|
108
|
+
});
|
|
109
|
+
if (r.status !== 0)
|
|
110
|
+
return null;
|
|
111
|
+
// Carry gitignored local config (e.g. .env) into the new tree — best-effort.
|
|
112
|
+
copyWorktreeIncludes(top, dest);
|
|
113
|
+
const branchNote = localExists ? '' : remoteExists ? ` (new, tracking origin/${branch})` : ' (new)';
|
|
114
|
+
return { dest, branch, branchNote };
|
|
115
|
+
}
|
|
27
116
|
function worktree(branch, opts = {}) {
|
|
28
117
|
const top = (0, git_1.gitToplevel)();
|
|
29
118
|
if (!top)
|
|
@@ -35,29 +124,78 @@ function worktree(branch, opts = {}) {
|
|
|
35
124
|
const dest = node_path_1.default.resolve(opts.path || node_path_1.default.join(node_path_1.default.dirname(top), `${base}-${safeBranch}`));
|
|
36
125
|
if (node_fs_1.default.existsSync(dest))
|
|
37
126
|
(0, ctx_1.die)(`destination already exists: ${dest}`);
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// local tracking branch; otherwise → new branch from --from (or HEAD).
|
|
42
|
-
const args = localExists
|
|
43
|
-
? ['worktree', 'add', dest, branch]
|
|
44
|
-
: remoteExists
|
|
45
|
-
? ['worktree', 'add', '-b', branch, dest, `origin/${branch}`]
|
|
46
|
-
: ['worktree', 'add', '-b', branch, dest, opts.from || 'HEAD'];
|
|
47
|
-
const r = (0, node_child_process_1.spawnSync)('git', args, { cwd: top, stdio: 'inherit' });
|
|
48
|
-
if (r.status !== 0)
|
|
49
|
-
(0, ctx_1.die)(`git worktree add failed (exit ${r.status ?? '?'})`);
|
|
50
|
-
const branchNote = localExists ? '' : remoteExists ? ` (new, tracking origin/${branch})` : ' (new)';
|
|
127
|
+
const res = provisionWorktree({ top: top, branch, dest, fromRef: opts.from });
|
|
128
|
+
if (!res)
|
|
129
|
+
(0, ctx_1.die)(`git worktree add failed`);
|
|
51
130
|
process.stdout.write([
|
|
52
131
|
``,
|
|
53
|
-
`✓ worktree ready: ${dest}`,
|
|
54
|
-
` branch: ${branch}${branchNote}`,
|
|
132
|
+
`✓ worktree ready: ${res.dest}`,
|
|
133
|
+
` branch: ${branch}${res.branchNote}`,
|
|
55
134
|
``,
|
|
56
135
|
`Start a FRESH agent session inside it so it gets its own Convene identity:`,
|
|
57
|
-
` cd ${dest}`,
|
|
136
|
+
` cd ${res.dest}`,
|
|
58
137
|
` # install deps for this package if needed, then launch your agent (e.g. \`claude\`)`,
|
|
59
138
|
``,
|
|
60
|
-
`Remove it when done: git worktree remove ${dest}`,
|
|
139
|
+
`Remove it when done: git worktree remove ${res.dest}`,
|
|
61
140
|
``,
|
|
62
141
|
].join('\n'));
|
|
63
142
|
}
|
|
143
|
+
/** A safe, filesystem-friendly token from an arbitrary string (for branch/dir names). */
|
|
144
|
+
function safeToken(s) {
|
|
145
|
+
return s.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'sess';
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The AUTO-ISOLATE provisioning entry point used by SessionStart when a session
|
|
149
|
+
* boots into a checkout that already has a live sibling. FAIL-OPEN end-to-end:
|
|
150
|
+
* returns {dest, branch} on success or null on ANY error — the boot proceeds
|
|
151
|
+
* normally on null.
|
|
152
|
+
*
|
|
153
|
+
* Strategy:
|
|
154
|
+
* 1. Best-effort refresh the base (`git fetch origin main`) so the new tree is
|
|
155
|
+
* based on fresh upstream, not a stale local tip. A fetch failure is ignored.
|
|
156
|
+
* 2. Pick the base ref: the current branch's upstream if it has one, else
|
|
157
|
+
* `origin/main` (falling back to `HEAD` only if neither resolves).
|
|
158
|
+
* 3. Compute a NON-colliding branch name `auto/<disc-or-member>-<short>` and a
|
|
159
|
+
* NON-colliding sibling destination, bumping a counter on collision.
|
|
160
|
+
* 4. provisionWorktree(...) (which also copies `.worktreeinclude`).
|
|
161
|
+
*/
|
|
162
|
+
function provisionAutoWorktree(top, slug) {
|
|
163
|
+
try {
|
|
164
|
+
// 1. Best-effort refresh of the canonical base.
|
|
165
|
+
(0, git_1.gitFetch)('main', 'origin', top);
|
|
166
|
+
// 2. Base ref: current branch's upstream, else origin/main, else HEAD.
|
|
167
|
+
let fromRef = 'origin/main';
|
|
168
|
+
if (!refExists('refs/remotes/origin/main', top))
|
|
169
|
+
fromRef = 'HEAD';
|
|
170
|
+
const cur = (0, git_1.currentBranch)(top);
|
|
171
|
+
if (cur) {
|
|
172
|
+
const up = (0, node_child_process_1.spawnSync)('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', `${cur}@{upstream}`], {
|
|
173
|
+
cwd: top,
|
|
174
|
+
encoding: 'utf8',
|
|
175
|
+
});
|
|
176
|
+
const upstream = up.status === 0 ? (up.stdout || '').trim() : '';
|
|
177
|
+
if (upstream && refExists(`refs/remotes/${upstream}`, top))
|
|
178
|
+
fromRef = upstream;
|
|
179
|
+
}
|
|
180
|
+
// 3. Non-colliding branch name + destination.
|
|
181
|
+
const disc = (0, git_1.sessionDiscriminator)();
|
|
182
|
+
const tag = safeToken(disc || slug);
|
|
183
|
+
const base = (0, git_1.worktreeBasename)(top);
|
|
184
|
+
const parent = node_path_1.default.dirname(top);
|
|
185
|
+
let branch = `auto/${tag}`;
|
|
186
|
+
let dest = node_path_1.default.join(parent, `${base}-auto-${tag}`);
|
|
187
|
+
let n = 1;
|
|
188
|
+
while (refExists(`refs/heads/${branch}`, top) || node_fs_1.default.existsSync(dest)) {
|
|
189
|
+
n += 1;
|
|
190
|
+
branch = `auto/${tag}-${n}`;
|
|
191
|
+
dest = node_path_1.default.join(parent, `${base}-auto-${tag}-${n}`);
|
|
192
|
+
if (n > 50)
|
|
193
|
+
return null; // pathological — give up rather than spin
|
|
194
|
+
}
|
|
195
|
+
const res = provisionWorktree({ top, branch, dest, fromRef, quiet: true });
|
|
196
|
+
return res ? { dest: res.dest, branch: res.branch } : null;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null; // fail-open: any error → no relocation, boot proceeds
|
|
200
|
+
}
|
|
201
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.CACHE_DIR = exports.CONFIG_FILE = exports.CONFIG_DIR = void 0;
|
|
7
7
|
exports.homeBase = homeBase;
|
|
8
|
+
exports.resolveFetchTimeoutMs = resolveFetchTimeoutMs;
|
|
9
|
+
exports.resolveWatchMaxMs = resolveWatchMaxMs;
|
|
8
10
|
exports.isWorldReadable = isWorldReadable;
|
|
9
11
|
exports.loadFileConfig = loadFileConfig;
|
|
10
12
|
exports.loadProjectConfig = loadProjectConfig;
|
|
@@ -28,6 +30,34 @@ const brand_1 = require("./brand");
|
|
|
28
30
|
function homeBase() {
|
|
29
31
|
return process.env.CONVENE_HOME_OVERRIDE || node_os_1.default.homedir();
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Network fetch timeout (ms) for the hook/catch-up paths. Overridable via
|
|
35
|
+
* CONVENE_FETCH_TIMEOUT_MS so the fail-open/timeout tests can drive the path
|
|
36
|
+
* deterministically with a small value instead of relying on the real 4s
|
|
37
|
+
* wall-clock — which, under full-suite serial load, collides with process-spawn
|
|
38
|
+
* + GC jitter and intermittently blows the latency-budget assertion. A garbage or
|
|
39
|
+
* non-positive value falls back to the default. Production default unchanged (4000).
|
|
40
|
+
*/
|
|
41
|
+
function resolveFetchTimeoutMs(fallback = 4000) {
|
|
42
|
+
const raw = process.env.CONVENE_FETCH_TIMEOUT_MS;
|
|
43
|
+
const n = raw ? Number(raw) : NaN;
|
|
44
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Hard ceiling (ms) on a single `convene watch` daemon's total lifetime — the
|
|
48
|
+
* absolute-runtime half of the leak fix (the other half is idle exit; see
|
|
49
|
+
* watch.ts). A detached watcher self-terminates once it has run this long so an
|
|
50
|
+
* orphaned daemon can never live forever (the 145-leaked-watchers bug). Mirrors
|
|
51
|
+
* resolveFetchTimeoutMs: a positive finite CONVENE_WATCH_MAX_MS wins (floored),
|
|
52
|
+
* else the fallback. Tests drive it tiny for a deterministic exit; the production
|
|
53
|
+
* default is 12h — comfortably longer than any real heads-down turn, and a fresh
|
|
54
|
+
* watcher is relaunched at the next SessionStart regardless.
|
|
55
|
+
*/
|
|
56
|
+
function resolveWatchMaxMs(fallback = 12 * 60 * 60 * 1000) {
|
|
57
|
+
const raw = process.env.CONVENE_WATCH_MAX_MS;
|
|
58
|
+
const n = raw ? Number(raw) : NaN;
|
|
59
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
60
|
+
}
|
|
31
61
|
exports.CONFIG_DIR = node_path_1.default.join(homeBase(), brand_1.BRAND.configDir);
|
|
32
62
|
exports.CONFIG_FILE = node_path_1.default.join(exports.CONFIG_DIR, 'config.json');
|
|
33
63
|
exports.CACHE_DIR = node_path_1.default.join(exports.CONFIG_DIR, 'cache');
|
package/dist/index.js
CHANGED
|
@@ -57,9 +57,11 @@ const lane_1 = require("./commands/lane");
|
|
|
57
57
|
const deploy_1 = require("./commands/deploy");
|
|
58
58
|
const guard_1 = require("./commands/guard");
|
|
59
59
|
const gate_push_1 = require("./commands/gate-push");
|
|
60
|
+
const beat_1 = require("./commands/beat");
|
|
60
61
|
const practice_guard_1 = require("./commands/practice-guard");
|
|
61
62
|
const override_1 = require("./commands/override");
|
|
62
63
|
const watch_1 = require("./commands/watch");
|
|
64
|
+
const watch_reap_1 = require("./commands/watch-reap");
|
|
63
65
|
const explain_1 = require("./commands/explain");
|
|
64
66
|
const practices_1 = require("./commands/practices");
|
|
65
67
|
const update_1 = require("./commands/update");
|
|
@@ -168,12 +170,22 @@ program
|
|
|
168
170
|
.option('--reason <text>', 'why the gate is being overridden (required; attributed to the bus)')
|
|
169
171
|
.option('--project <slug>')
|
|
170
172
|
.action((id, opts) => (0, override_1.override)(id, opts));
|
|
173
|
+
program
|
|
174
|
+
.command('beat')
|
|
175
|
+
.description('PostToolUse hook: debounced session activity-beat (presence), fail-open + fast')
|
|
176
|
+
.option('--stdin', 'read the PostToolUse JSON payload from stdin (to derive the coarse area)')
|
|
177
|
+
.action((opts) => (0, beat_1.beat)(opts));
|
|
171
178
|
program
|
|
172
179
|
.command('watch')
|
|
173
180
|
.description('SessionStart-launched detached long-poll for directed halts (fail-open, self-healing)')
|
|
174
181
|
.option('--notify', 'best-effort desktop notification per surfaced halt')
|
|
175
182
|
.option('--project <slug>')
|
|
176
183
|
.action((opts) => (0, watch_1.watch)(opts));
|
|
184
|
+
program
|
|
185
|
+
.command('watch-reap')
|
|
186
|
+
.description('reap orphaned (PID-1) detached `convene watch` daemons (POSIX-only, fail-open)')
|
|
187
|
+
.option('--dry-run', 'list reapable watchers; kill nothing')
|
|
188
|
+
.action((opts) => (0, watch_reap_1.watchReap)({ dryRun: opts.dryRun }));
|
|
177
189
|
program
|
|
178
190
|
.command('notify-push')
|
|
179
191
|
.description('git pre-push hook: post a [STATUS] summarizing the push (fail-silent)')
|
package/dist/render.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SESSION_OPEN_TAG = exports.UNTRUSTED_LABEL = void 0;
|
|
3
|
+
exports.RELOCATE_TAG = exports.SESSION_OPEN_TAG = exports.UNTRUSTED_LABEL = void 0;
|
|
4
4
|
exports.inertToken = inertToken;
|
|
5
5
|
exports.renderHealthLine = renderHealthLine;
|
|
6
6
|
exports.renderRecentLine = renderRecentLine;
|
|
7
7
|
exports.renderOpenItem = renderOpenItem;
|
|
8
8
|
exports.renderChannelBlock = renderChannelBlock;
|
|
9
9
|
exports.renderSessionOpenBlock = renderSessionOpenBlock;
|
|
10
|
+
exports.renderRelocateBlock = renderRelocateBlock;
|
|
10
11
|
/**
|
|
11
12
|
* The `<convene-channel>` render contract (P0-RENDER) + untrusted-prompt fencing
|
|
12
13
|
* (P0-PROMPT). This is the ONE place the agent-facing text grammar is produced,
|
|
@@ -176,6 +177,31 @@ function renderLaneHaltSection(L, lanes, halts) {
|
|
|
176
177
|
L.push('');
|
|
177
178
|
}
|
|
178
179
|
}
|
|
180
|
+
/** Compact "Active sessions" block — a faint pulse of who else is working now. */
|
|
181
|
+
function renderPresenceSection(L, presence) {
|
|
182
|
+
if (!presence.length)
|
|
183
|
+
return;
|
|
184
|
+
L.push('Active sessions (working now):');
|
|
185
|
+
for (const p of presence.slice(0, 8)) {
|
|
186
|
+
const bits = [inertToken(p.session, 64)];
|
|
187
|
+
if (p.area)
|
|
188
|
+
bits.push(inertToken(p.area, 32));
|
|
189
|
+
if (p.edits)
|
|
190
|
+
bits.push(`${p.edits} edit${p.edits === 1 ? '' : 's'}`);
|
|
191
|
+
if (p.ageSec != null && Number.isFinite(p.ageSec))
|
|
192
|
+
bits.push(presenceAge(p.ageSec));
|
|
193
|
+
L.push(' · ' + bits.join(' · '));
|
|
194
|
+
}
|
|
195
|
+
L.push('');
|
|
196
|
+
}
|
|
197
|
+
function presenceAge(sec) {
|
|
198
|
+
const s = Math.max(0, Math.round(sec));
|
|
199
|
+
if (s < 60)
|
|
200
|
+
return `${s}s ago`;
|
|
201
|
+
if (s < 3600)
|
|
202
|
+
return `${Math.round(s / 60)}m ago`;
|
|
203
|
+
return `${Math.round(s / 3600)}h ago`;
|
|
204
|
+
}
|
|
179
205
|
function renderChannelBlock(input) {
|
|
180
206
|
const { slug, member, session, lookbackMin, openItems, recent, health } = input;
|
|
181
207
|
const lanes = input.lanes ?? [];
|
|
@@ -204,6 +230,7 @@ function renderChannelBlock(input) {
|
|
|
204
230
|
// High-priority interrupts + active deploy lanes, above Open items (DEGRADED
|
|
205
231
|
// suppresses these structurally — the caller passes empty arrays).
|
|
206
232
|
renderLaneHaltSection(L, lanes, halts);
|
|
233
|
+
renderPresenceSection(L, input.presence ?? []);
|
|
207
234
|
L.push('Open items for you:');
|
|
208
235
|
if (openItems.length === 0) {
|
|
209
236
|
L.push(' (none)');
|
|
@@ -295,3 +322,20 @@ function renderSessionOpenBlock(input) {
|
|
|
295
322
|
L.push(`</${exports.SESSION_OPEN_TAG}>`);
|
|
296
323
|
return L.join('\n');
|
|
297
324
|
}
|
|
325
|
+
// ── <convene-relocate> (auto-isolate) ────────────────────────────────────────
|
|
326
|
+
exports.RELOCATE_TAG = 'convene-relocate';
|
|
327
|
+
/**
|
|
328
|
+
* The `<convene-relocate>` block emitted at SessionStart when this session booted
|
|
329
|
+
* into a checkout that already has a live sibling and a clean isolated worktree was
|
|
330
|
+
* provisioned for it. It is an IMPERATIVE, NON-alarming instruction the agent acts
|
|
331
|
+
* on — framed as "a tree is ready, move there", not a warning. Deliberately no
|
|
332
|
+
* "WARNING"; the move is the recommended, friction-free path.
|
|
333
|
+
*/
|
|
334
|
+
function renderRelocateBlock(input) {
|
|
335
|
+
const { dest, branch } = input;
|
|
336
|
+
return (`<${exports.RELOCATE_TAG}>\n` +
|
|
337
|
+
`Another live session is active in this checkout. A clean isolated worktree is ready for you ` +
|
|
338
|
+
`at ${dest} (branch ${branch}, based on fresh origin/main, gitignored config copied). ` +
|
|
339
|
+
`To avoid clobbering the other session's files, relocate now: run \`cd ${dest}\` and continue your work there.\n` +
|
|
340
|
+
`</${exports.RELOCATE_TAG}>`);
|
|
341
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "convene-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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",
|