claude-code-session-manager 0.120.0 → 0.122.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/assets/{AgentLibrary-LBg3mVL8.js → AgentLibrary-BmJ8hIPw.js} +1 -1
- package/dist/assets/{Button-C4BN5ISR.js → Button-BS4kmz4p.js} +1 -1
- package/dist/assets/{DataModel-CHhjk61s.js → DataModel-DZHBZrfL.js} +1 -1
- package/dist/assets/History-pg00b2Gd.js +2 -0
- package/dist/assets/{Hooks-ByWVgXIf.js → Hooks-BTEs0UZY.js} +1 -1
- package/dist/assets/{Library-QvYVAEmY.js → Library-DuaJ-VSn.js} +1 -1
- package/dist/assets/{ListDetail-FvNe6fo5.js → ListDetail-Cud72s-O.js} +1 -1
- package/dist/assets/{McpServers-C7EmPm8B.js → McpServers-Dy-wnG_X.js} +1 -1
- package/dist/assets/{Memory-nyLR4Yw4.js → Memory-Bs1T1Jo7.js} +1 -1
- package/dist/assets/{Panel-thElCbLX.js → Panel-8UhLXEOW.js} +1 -1
- package/dist/assets/{Permissions-FQ9GM6Ca.js → Permissions-BfwzvmBS.js} +1 -1
- package/dist/assets/Plugins-McjyvvKh.js +2 -0
- package/dist/assets/{ProvenanceBadge-lAViG8yV.js → ProvenanceBadge-D0Ov_6WJ.js} +1 -1
- package/dist/assets/{RemoteMachines-AWpARk7N.js → RemoteMachines-gOlFTvWq.js} +1 -1
- package/dist/assets/{SaveBar-c15EEGB_.js → SaveBar-Cd6hJR6K.js} +1 -1
- package/dist/assets/Scheduler-BZXKqYJP.js +16 -0
- package/dist/assets/{ScopeSwitcher-BLep1rtB.js → ScopeSwitcher-C24ysV1K.js} +1 -1
- package/dist/assets/{Settings-D1kTUIDB.js → Settings-DWnN2qxn.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-_40_f0t3.js → SkillReferenceGraph-BsWuiTbw.js} +1 -1
- package/dist/assets/{Skills-DaTkSOuU.js → Skills-6pHn6glu.js} +1 -1
- package/dist/assets/{SystemPrompt-DxNf-soM.js → SystemPrompt-DT0LBH-K.js} +1 -1
- package/dist/assets/{TagLibrary-L6KpBV-x.js → TagLibrary-DbJ0zbYL.js} +1 -1
- package/dist/assets/{TiptapBody-C_XjtJ82.js → TiptapBody-C4kIvIMH.js} +1 -1
- package/dist/assets/{Toggle-H0rERKNI.js → Toggle-pP19zlyk.js} +1 -1
- package/dist/assets/{ViewTabs-ZUWphDat.js → ViewTabs-CpzN8e5I.js} +1 -1
- package/dist/assets/{historyTotals-BxFKOztb.js → historyTotals-Caq78bb5.js} +1 -1
- package/dist/assets/{index-B5wj0DEX.js → index-Bjtguv6I.js} +434 -434
- package/dist/assets/{settingsSchema-BRcYkXJN.js → settingsSchema-CC2ySu3d.js} +1 -1
- package/dist/assets/{skillFrontmatter-DFGp4DST.js → skillFrontmatter-hDhNo8jE.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-VvrgalfC.js → useAgentsDirWatch-BuOtY25U.js} +1 -1
- package/dist/assets/{useRemotePeers-mvXKNEE3.js → useRemotePeers-CNQmRBpt.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -7
- package/plugins/session-manager-dev/skills/develop/standards.md +4 -10
- package/scripts/scheduler-mcp-server.cjs +2 -2
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +56 -43
- package/src/main/docEdit.cjs +1 -0
- package/src/main/index.cjs +12 -13
- package/src/main/ipcSchemas.cjs +17 -3
- package/src/main/lib/classifyPromptTicket.cjs +1 -0
- package/src/main/lib/claude/effectiveModelInfo.cjs +9 -21
- package/src/main/lib/claude/personaToolScope.cjs +7 -1
- package/src/main/lib/claude/runClaudeP.cjs +1 -0
- package/src/main/lib/epic/delegationReadiness.cjs +75 -8
- package/src/main/lib/mcpToolCatalog.cjs +0 -1
- package/src/main/lib/persona-macro/macroLibrary.cjs +75 -0
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +11 -0
- package/src/main/lib/prd/prdCreate.cjs +28 -1
- package/src/main/lib/prd/prdSizing.cjs +1 -8
- package/src/main/lib/remoteTranscriptTail.cjs +69 -2
- package/src/main/lib/runSeeders.cjs +21 -0
- package/src/main/lib/scheduler/loopEfficiency.cjs +109 -0
- package/src/main/lib/scheduler/queueHistory.cjs +44 -1
- package/src/main/lib/scheduler/queueStore.cjs +4 -72
- package/src/main/lib/scheduler/rcaReport.cjs +1 -1
- package/src/main/lib/scheduler/refireCap.cjs +40 -0
- package/src/main/lib/scheduler/reviewNotice.cjs +1 -1
- package/src/main/lib/scheduler/schedulerConfig.cjs +1 -2
- package/src/main/lib/scheduler/schedulerPaths.cjs +0 -2
- package/src/main/lib/scheduler/sessionSlots.cjs +1 -1
- package/src/main/lib/telemetry/auditLog.cjs +65 -14
- package/src/main/loopEfficiencyHandlers.cjs +160 -0
- package/src/main/queueOps.cjs +4 -9
- package/src/main/runVerify.cjs +22 -454
- package/src/main/scheduler/reviewLadder.cjs +12 -2
- package/src/main/scheduler.cjs +103 -177
- package/src/main/seedDevPlugin.cjs +49 -22
- package/src/main/seedStatus.cjs +3 -2
- package/src/main/supervisor.cjs +1 -0
- package/src/main/templates/PRD_AUTHORING.md +16 -31
- package/src/main/webRemote.cjs +82 -91
- package/src/preload/api.d.ts +39 -6
- package/src/preload/index.cjs +3 -0
- package/src/seed/agents/dev-lead.md +2 -9
- package/dist/assets/History-CL-T6Lv3.js +0 -2
- package/dist/assets/Plugins-D9KE7XTI.js +0 -2
- package/dist/assets/Scheduler-FvduP1FG.js +0 -16
|
@@ -29,21 +29,21 @@
|
|
|
29
29
|
const fs = require('node:fs');
|
|
30
30
|
const path = require('node:path');
|
|
31
31
|
const os = require('node:os');
|
|
32
|
-
const { install, update } = require('./pluginInstall.cjs');
|
|
32
|
+
const { install, update, bundledMarketplaceDir } = require('./pluginInstall.cjs');
|
|
33
33
|
|
|
34
34
|
const PLUGIN_SLUG = 'session-manager-dev';
|
|
35
35
|
const MARKETPLACE = { add: 'bundled', name: 'session-manager' };
|
|
36
36
|
const ENABLED_KEY = `${PLUGIN_SLUG}@${MARKETPLACE.name}`;
|
|
37
37
|
const MAX_ATTEMPTS = 3; // give a transient first-boot failure a few chances
|
|
38
38
|
|
|
39
|
-
function markerPath() {
|
|
40
|
-
return path.join(
|
|
39
|
+
function markerPath(home = os.homedir()) {
|
|
40
|
+
return path.join(home, '.claude', 'session-manager', '.dev-plugin-seeded');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Read the marker → { done:boolean, attempts:number }. Absent = fresh. */
|
|
44
|
-
function readMarker() {
|
|
44
|
+
function readMarker(home) {
|
|
45
45
|
try {
|
|
46
|
-
const raw = fs.readFileSync(markerPath(), 'utf8').trim();
|
|
46
|
+
const raw = fs.readFileSync(markerPath(home), 'utf8').trim();
|
|
47
47
|
const m = JSON.parse(raw);
|
|
48
48
|
return { done: !!m.done, attempts: Number(m.attempts) || 0 };
|
|
49
49
|
} catch {
|
|
@@ -51,9 +51,9 @@ function readMarker() {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function alreadyEnabled() {
|
|
54
|
+
function alreadyEnabled(home = os.homedir()) {
|
|
55
55
|
try {
|
|
56
|
-
const raw = fs.readFileSync(path.join(
|
|
56
|
+
const raw = fs.readFileSync(path.join(home, '.claude', 'settings.json'), 'utf8');
|
|
57
57
|
const enabled = JSON.parse(raw)?.enabledPlugins;
|
|
58
58
|
return !!(enabled && enabled[ENABLED_KEY]);
|
|
59
59
|
} catch {
|
|
@@ -61,9 +61,9 @@ function alreadyEnabled() {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function writeMarker(state) {
|
|
64
|
+
function writeMarker(state, home) {
|
|
65
65
|
try {
|
|
66
|
-
const p = markerPath();
|
|
66
|
+
const p = markerPath(home);
|
|
67
67
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
68
68
|
fs.writeFileSync(p, JSON.stringify({ ...state, ts: new Date().toISOString() }) + '\n');
|
|
69
69
|
} catch (err) {
|
|
@@ -87,6 +87,24 @@ function readJson(p) {
|
|
|
87
87
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Pure check: is the registered 'session-manager' marketplace source missing on disk or
|
|
92
|
+
* different from `expectedDir`? Not registered at all → false (a deliberate uninstall stays).
|
|
93
|
+
* (Own reader: delegationReadiness's readDevMarketplacePath is not exported.)
|
|
94
|
+
*/
|
|
95
|
+
function marketplaceIsStale({ homeDir = os.homedir(), expectedDir = bundledMarketplaceDir() } = {}) {
|
|
96
|
+
const m = readJson(path.join(homeDir, '.claude', 'plugins', 'known_marketplaces.json'))?.[MARKETPLACE.name];
|
|
97
|
+
const p = m?.source?.path ?? m?.installLocation;
|
|
98
|
+
if (typeof p !== 'string' || !p) return false;
|
|
99
|
+
if (path.resolve(p) !== path.resolve(expectedDir)) return true;
|
|
100
|
+
return !fs.existsSync(p);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** True only when settings.json explicitly sets the plugin to false. */
|
|
104
|
+
function deliberatelyDisabled(home) {
|
|
105
|
+
return readJson(path.join(home, '.claude', 'settings.json'))?.enabledPlugins?.[ENABLED_KEY] === false;
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
function bundledVersion() {
|
|
91
109
|
const m = readJson(path.join(__dirname, '..', '..', 'plugins', PLUGIN_SLUG, '.claude-plugin', 'plugin.json'));
|
|
92
110
|
return typeof m?.version === 'string' ? m.version : null;
|
|
@@ -131,23 +149,32 @@ async function upgradeIfStale({
|
|
|
131
149
|
}
|
|
132
150
|
}
|
|
133
151
|
|
|
134
|
-
async function seedDevPlugin({
|
|
152
|
+
async function seedDevPlugin({
|
|
153
|
+
logger = console,
|
|
154
|
+
writeLog = () => {},
|
|
155
|
+
home = os.homedir(),
|
|
156
|
+
expectedDir = bundledMarketplaceDir(),
|
|
157
|
+
runInstall = install,
|
|
158
|
+
runUpdate = update,
|
|
159
|
+
} = {}) {
|
|
135
160
|
if (process.env.SM_SEED_DEV_PLUGIN_DISABLE === '1') return;
|
|
136
|
-
await upgradeIfStale({ logger, writeLog });
|
|
137
|
-
const marker = readMarker();
|
|
138
|
-
|
|
139
|
-
|
|
161
|
+
await upgradeIfStale({ logger, writeLog, home, runUpdate });
|
|
162
|
+
const marker = readMarker(home);
|
|
163
|
+
// Repair: the registered marketplace dir vanished or points at an older package dir.
|
|
164
|
+
const repair = marker.done && !deliberatelyDisabled(home) && marketplaceIsStale({ homeDir: home, expectedDir });
|
|
165
|
+
if (marker.done && !repair) return; // succeeded before — leave it alone.
|
|
166
|
+
if (!marker.done && marker.attempts >= MAX_ATTEMPTS) return; // gave up — manual install only.
|
|
140
167
|
|
|
141
168
|
try {
|
|
142
|
-
if (alreadyEnabled()) {
|
|
143
|
-
writeMarker({ done: true, attempts: marker.attempts });
|
|
169
|
+
if (!repair && alreadyEnabled(home)) {
|
|
170
|
+
writeMarker({ done: true, attempts: marker.attempts }, home);
|
|
144
171
|
return;
|
|
145
172
|
}
|
|
146
|
-
logger.log?.(
|
|
147
|
-
const r = await
|
|
173
|
+
logger.log?.(`[seedDevPlugin] ${repair ? 're-registering' : 'installing'} bundled session-manager-dev plugin…`);
|
|
174
|
+
const r = await runInstall({ slug: PLUGIN_SLUG, marketplace: MARKETPLACE });
|
|
148
175
|
if (r.ok) {
|
|
149
176
|
logger.log?.('[seedDevPlugin] installed session-manager-dev@session-manager');
|
|
150
|
-
writeMarker({ done: true, attempts: marker.attempts });
|
|
177
|
+
writeMarker({ done: true, attempts: marker.attempts }, home);
|
|
151
178
|
} else {
|
|
152
179
|
// Failure: bump the attempt counter so the next boot can retry (bounded).
|
|
153
180
|
logger.warn?.(`[seedDevPlugin] install failed (exit ${r.exitCode})${r.error ? ` — ${r.error}` : ''}; attempt ${marker.attempts + 1}/${MAX_ATTEMPTS}`);
|
|
@@ -157,7 +184,7 @@ async function seedDevPlugin({ logger = console, writeLog = () => {} } = {}) {
|
|
|
157
184
|
message: 'install failed',
|
|
158
185
|
meta: { exitCode: r.exitCode, error: r.error, attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
|
|
159
186
|
});
|
|
160
|
-
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
187
|
+
writeMarker({ done: false, attempts: marker.attempts + 1 }, home);
|
|
161
188
|
}
|
|
162
189
|
} catch (err) {
|
|
163
190
|
logger.warn?.('[seedDevPlugin] error:', err?.message ?? err);
|
|
@@ -167,8 +194,8 @@ async function seedDevPlugin({ logger = console, writeLog = () => {} } = {}) {
|
|
|
167
194
|
message: 'seed error',
|
|
168
195
|
meta: { error: err?.message ?? String(err), attempt: marker.attempts + 1, maxAttempts: MAX_ATTEMPTS },
|
|
169
196
|
});
|
|
170
|
-
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
197
|
+
writeMarker({ done: false, attempts: marker.attempts + 1 }, home);
|
|
171
198
|
}
|
|
172
199
|
}
|
|
173
200
|
|
|
174
|
-
module.exports = { seedDevPlugin, upgradeIfStale, compareSemver, markerPath, MAX_ATTEMPTS };
|
|
201
|
+
module.exports = { seedDevPlugin, upgradeIfStale, marketplaceIsStale, compareSemver, markerPath, MAX_ATTEMPTS };
|
package/src/main/seedStatus.cjs
CHANGED
|
@@ -22,14 +22,15 @@ const SEEDERS = {
|
|
|
22
22
|
'agent-personas': seedAgentPersonas,
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
/** One seeder's marker → 'done' | 'pending' | 'exhausted'. */
|
|
25
|
+
/** One seeder's marker → 'done' | 'pending' | 'failed' | 'exhausted'. */
|
|
26
26
|
function statusFromMarker(mod) {
|
|
27
27
|
try {
|
|
28
28
|
const raw = fs.readFileSync(mod.markerPath(), 'utf8').trim();
|
|
29
29
|
const m = JSON.parse(raw);
|
|
30
30
|
if (m && m.done) return 'done';
|
|
31
31
|
const attempts = Number(m && m.attempts) || 0;
|
|
32
|
-
|
|
32
|
+
if (attempts >= mod.MAX_ATTEMPTS) return 'exhausted';
|
|
33
|
+
return attempts > 0 ? 'failed' : 'pending';
|
|
33
34
|
} catch {
|
|
34
35
|
return 'pending'; // absent or unparseable — never surfaced as an error.
|
|
35
36
|
}
|
package/src/main/supervisor.cjs
CHANGED
|
@@ -254,6 +254,7 @@ async function runProbe(claudeBin, prompt) {
|
|
|
254
254
|
'--max-budget-usd', '0.50',
|
|
255
255
|
'--dangerously-skip-permissions',
|
|
256
256
|
...headlessPermissionArgs(),
|
|
257
|
+
'--strict-mcp-config',
|
|
257
258
|
'--allowedTools', 'Bash',
|
|
258
259
|
], { env: withProcRole(process.env, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
|
|
259
260
|
} catch (e) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<!-- PRD_AUTHORING.md
|
|
1
|
+
<!-- PRD_AUTHORING.md v9 -->
|
|
2
2
|
# PRD Authoring Guide — Scheduler Safety Rules
|
|
3
3
|
|
|
4
4
|
Rule: every PRD you queue follows the rules below.
|
|
5
5
|
Why: two real stuck jobs (fizzpop poll-hang, etch-engine post-AC overrun) cost hours of wall-clock time and real money.
|
|
6
6
|
|
|
7
|
-
Before queueing, run the §
|
|
7
|
+
Before queueing, run the §14 checklist at the bottom. It is last on purpose — check it last.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -69,7 +69,7 @@ if (!found) {
|
|
|
69
69
|
Rule: every PRD needs these fields.
|
|
70
70
|
1. `title` — one line, plain English.
|
|
71
71
|
2. `cwd` — the Epic's project root, as an absolute path or `~/…`. The API always sets it to the Epic's project, whatever you pass. To work in another project, open an Epic in that project. The folder must exist when you queue.
|
|
72
|
-
3. `estimateMinutes` —
|
|
72
|
+
3. `estimateMinutes` — optional; omit it. A run without one gets the 45-minute kill-budget floor. Pass one only if the run legitimately needs longer. Most PRDs take 5–10 minutes (§8).
|
|
73
73
|
|
|
74
74
|
`parallelGroup` is deprecated and ignored. `dependsOn: [<slug>, ...]` is the only ordering primitive.
|
|
75
75
|
|
|
@@ -125,7 +125,7 @@ A short Epic-context digest is prepended automatically for orientation only —
|
|
|
125
125
|
|
|
126
126
|
The executor can't ask you anything: no `ScheduleWakeup`, Cron, Monitor, `AskUserQuestion`, plan mode, or worktree tools; background tasks are off (a `timeout` command is always on PATH — the app installs a shim on macOS). It must never stop to ask a question — it makes the safest reasonable call and reports it, so write the PRD so that call is obvious.
|
|
127
127
|
|
|
128
|
-
Worktree reach: every PRD runs in an isolated git worktree holding tracked files plus `.venv`/`venv` (and `node_modules` for non-pnpm projects) symlinked from the main checkout; pnpm projects (pnpm-lock.yaml) get a worktree-local `node_modules` installed offline from the store. Why: a pnpm install through the symlink purged the main checkout's node_modules (Bilko-Game-Academy 2026-10-18). A step that needs gitignored runtime state (databases, `downloads/`, browser profiles, logs) or a live side effect must address the main checkout by absolute path (e.g. `$HOME/Projects/<repo>/...`), be artifact-only (a deliverable artifact + `artifactPaths`, no tracked edits), and `dependsOn` the PRD that lands its tracked edits. Host crontab installs stay human-only (see §
|
|
128
|
+
Worktree reach: every PRD runs in an isolated git worktree holding tracked files plus `.venv`/`venv` (and `node_modules` for non-pnpm projects) symlinked from the main checkout; pnpm projects (pnpm-lock.yaml) get a worktree-local `node_modules` installed offline from the store. Why: a pnpm install through the symlink purged the main checkout's node_modules (Bilko-Game-Academy 2026-10-18). A step that needs gitignored runtime state (databases, `downloads/`, browser profiles, logs) or a live side effect must address the main checkout by absolute path (e.g. `$HOME/Projects/<repo>/...`), be artifact-only (a deliverable artifact + `artifactPaths`, no tracked edits), and `dependsOn` the PRD that lands its tracked edits. Host crontab installs stay human-only (see §14).
|
|
129
129
|
Why: burrow PRD 1516 halted because its relative `.venv` python against `downloads/fb-tracker.db`, `scripts/install-cron.sh` and `pipeline-cron.sh --now` resolved inside the worktree (no runtime state; install-cron would bake a temporary worktree path into crontab), while sibling 1494 succeeded the same day using an absolute main-checkout path and artifact-only output. `scheduler_create_prd` warns on relative gitignored paths.
|
|
130
130
|
|
|
131
131
|
---
|
|
@@ -155,7 +155,7 @@ Note: a `rateLimited` exit-1 is the scheduler's own benign auto-pause (it resume
|
|
|
155
155
|
## §10 Negative-assertion checks must exit 0 on the clean case
|
|
156
156
|
|
|
157
157
|
Rule: a check that asserts something is ABSENT must exit 0 when it's absent. Write it as `if <detector>; then echo HALT...; exit 1; fi` — never leave the no-match path carrying the non-zero exit.
|
|
158
|
-
Why: `grep` exits 1 when it finds nothing. PRD `62-x-trader-doctrine` cleaned up correctly, but its sanity `grep` for a banned phrase found nothing, exited 1, and
|
|
158
|
+
Why: `grep` exits 1 when it finds nothing. PRD `62-x-trader-doctrine` cleaned up correctly, but its sanity `grep` for a banned phrase found nothing, exited 1, and the gate command itself failed on a clean tree.
|
|
159
159
|
|
|
160
160
|
```bash
|
|
161
161
|
if grep -rniE "banned phrase" path/; then
|
|
@@ -168,10 +168,10 @@ Applies to `grep`, `rg`, `diff` (exits 1 on any difference), and any custom dete
|
|
|
168
168
|
|
|
169
169
|
---
|
|
170
170
|
|
|
171
|
-
## §11 End green
|
|
171
|
+
## §11 End green
|
|
172
172
|
|
|
173
|
-
Rule: order the run so the acceptance/test gate is the LAST command. Run any intentionally-failing step (a TDD red test, an expected-nonzero probe) EARLY, and capture its output (`2>&1 | tail`
|
|
174
|
-
Why: the
|
|
173
|
+
Rule: order the run so the acceptance/test gate is the LAST command. Run any intentionally-failing step (a TDD red test, an expected-nonzero probe) EARLY, and capture its output (`2>&1 | tail`).
|
|
174
|
+
Why: the last command's result is the run's real outcome; a red step left at the end hides whether the work is green.
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
177
|
# RIGHT — red demo first and captured, green gate last
|
|
@@ -180,38 +180,24 @@ timeout 120 python -m pytest tests/test_repro.py::test_bug 2>&1 | tail -3 || tru
|
|
|
180
180
|
timeout 300 pytest -q # LAST thing the run does
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
-
The scheduler appends a finish protocol after your PRD's own steps — you never write it: review, then the `# Gate` commands (§6), then commit only the exact paths you created or changed for this PRD, then the verdict line — `SCHEDULER_VERDICT: PASS` once the gate is green and the commit landed, else `SCHEDULER_VERDICT: FAIL <one-line reason>`.
|
|
183
|
+
The scheduler appends a finish protocol after your PRD's own steps — you never write it: review, then the `# Gate` commands (§6), then commit only the exact paths you created or changed for this PRD, then the verdict line — `SCHEDULER_VERDICT: PASS` once the gate is green and the commit landed, else `SCHEDULER_VERDICT: FAIL <one-line reason>`. Never print `PASS` on a red gate.
|
|
184
184
|
|
|
185
185
|
---
|
|
186
186
|
|
|
187
|
-
## §12
|
|
188
|
-
|
|
189
|
-
Rule: a throwaway probe that errors (a bad quote, a wrong kwarg) must be re-run corrected right after, or annotated `# expected/handled: <why>` on the next line — never left stranded. Prefer a temp `.py` file over a fragile inline `python -c` one-liner.
|
|
190
|
-
|
|
191
|
-
Rule: an *expected* `timeout` cap (a long ingest/scan you expect to hit it) is success-with-note, not a bare `Exit code 124` — branch on it explicitly:
|
|
192
|
-
```bash
|
|
193
|
-
timeout 120 python -m project.ingest --all || { rc=$?
|
|
194
|
-
[ $rc -eq 124 ] && echo "hit time cap — partial, rows persist; OK" \
|
|
195
|
-
|| { echo "HALT: ingest failed rc=$rc"; exit 1; }; }
|
|
196
|
-
```
|
|
197
|
-
Why: both a stranded probe traceback and a bare `Exit code 124` read as failure to the verifier even when the committed work is correct and green (PRDs 77, 80 — 2026-06-13).
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## §13 Queueing PRDs from external automation
|
|
187
|
+
## §12 Queueing PRDs from external automation
|
|
202
188
|
|
|
203
189
|
Decision: is the session-manager app running on this machine right now?
|
|
204
|
-
- Yes → call the `scheduler_create_prd` MCP tool, with the usual fields (`title`, `cwd`, `
|
|
190
|
+
- Yes → call the `scheduler_create_prd` MCP tool, with the usual fields (`title`, `cwd`, `sourcePromptId`, `goal`, `acceptanceCriteria`, `implementationNotes`) plus `gate` and `files` (§6).
|
|
205
191
|
- No → stop. Ask the human to start the app, then call the tool again. Never write the PRD file by hand: the scheduler quarantines a file the API did not write, and it never runs.
|
|
206
192
|
|
|
207
193
|
It only works while the app is running — closing it removes the admin port/token, and the call fails with `session-manager app is not running (admin API unreachable)`.
|
|
208
194
|
|
|
209
195
|
---
|
|
210
196
|
|
|
211
|
-
## §
|
|
197
|
+
## §13 A parked job may resolve itself now
|
|
212
198
|
|
|
213
|
-
Rule: if a job parks `needs_review` with verdict `
|
|
214
|
-
Why: those
|
|
199
|
+
Rule: if a job parks `needs_review` with verdict `no_verdict_sentinel` or `abandoned_background_task`, do nothing first. The scheduler re-runs the gate on its own and completes the job once the gate is green, the commit is on HEAD, and the tracked tree is clean.
|
|
200
|
+
Why: those verdicts mean the run ended without a clean verdict line, not that the work was wrong.
|
|
215
201
|
|
|
216
202
|
Rule: a park caused by a DIFFERENT actor already finishing the same objective (a sibling PRD, a human) does not self-heal this way. Why: the job has no landed commit of its own. Confirm in the tree that the work is really done, then call `scheduler_archive_prd` with the slug. Archiving marks the job completed and frees the PRDs that depend on it. Never hand-edit `queue.json`: the scheduler and the watchdog both write it. Archiving clears a stale label; it does not fix a real bug.
|
|
217
203
|
|
|
@@ -219,7 +205,7 @@ Known gap: `pass_no_commit` ("already correct, nothing to commit") isn't yet in
|
|
|
219
205
|
|
|
220
206
|
---
|
|
221
207
|
|
|
222
|
-
## §
|
|
208
|
+
## §14 Pre-queue checklist (the litany)
|
|
223
209
|
|
|
224
210
|
Before queueing a new PRD, verify each of these:
|
|
225
211
|
|
|
@@ -228,7 +214,7 @@ Before queueing a new PRD, verify each of these:
|
|
|
228
214
|
- [ ] **§2 No bonus work:** the AC list is the only source of work.
|
|
229
215
|
- [ ] **§3 Verify, don't poll:** every deploy/migration step is followed by a test command that exits 1 on failure; the AC test command ran green once before you declare done.
|
|
230
216
|
- [ ] **§4 Bounded generators:** any search/seed loop has an explicit max and surfaces failure on exhaustion.
|
|
231
|
-
- [ ] **§5 Frontmatter:** `title`, `cwd` (exists on this machine)
|
|
217
|
+
- [ ] **§5 Frontmatter:** `title`, `cwd` (exists on this machine); `estimateMinutes` omitted (optional); `dependsOn` used for ordering, not `parallelGroup`.
|
|
232
218
|
- [ ] **§6 Gate and files:** `gate` follows every rule in §6 (or is `["none"]` alone); `files` is 1–50 repo-relative paths, no overlap with a concurrent PRD.
|
|
233
219
|
- [ ] **§7 Self-contained:** no reference to "the conversation" or outside context; paths/identifiers inline; clean UTF-8, no NUL bytes (`grep -qP '\x00' file && echo BAD`).
|
|
234
220
|
- [ ] **§7 Worktree reach:** no relative path to a gitignored file; live-run steps use absolute main-checkout paths in an artifact-only PRD. Why: the job worktree has no gitignored runtime state, so relative paths fail or bake a temporary path into host config (burrow 1516).
|
|
@@ -236,6 +222,5 @@ Before queueing a new PRD, verify each of these:
|
|
|
236
222
|
- [ ] **§9 Failure surfacing:** errors exit 1 with a diagnostic line; no silent `|| true`.
|
|
237
223
|
- [ ] **§10 Negative assertions:** every "should find nothing" check is inverted to exit 0 on the clean case.
|
|
238
224
|
- [ ] **§11 End green:** the gate runs last; any intentional failure runs early and is captured.
|
|
239
|
-
- [ ] **§12 No stranded probes:** a probe error is corrected or annotated; an expected `timeout` cap branches on exit 124.
|
|
240
225
|
- [ ] **Gate baseline:** every gate command ran green once at HEAD in the main checkout; a red suite is scoped to the PRD's own test files and the PRD notes say why. Why: a gate already red on base parks as `sentinel_fail`.
|
|
241
226
|
- [ ] **No human-only gates:** no gate or criterion needs a human or the passage of time (cron install, live post wait, live payment, dashboard action); those end `SCHEDULER_VERDICT: NEEDS_HUMAN <question>`. Why: no executor can pass them.
|
package/src/main/webRemote.cjs
CHANGED
|
@@ -29,7 +29,6 @@ const logs = require('./logs.cjs');
|
|
|
29
29
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
30
30
|
const { claudeHome } = require('./lib/scheduler/schedulerPaths.cjs');
|
|
31
31
|
const { schemas } = require('./ipcSchemas.cjs');
|
|
32
|
-
const { summarize: _sharedSummarize } = require('./lib/summarize.cjs');
|
|
33
32
|
const { makeState, confirmSas: confirmSasLogic } = require('./lib/e2eStateMachine.cjs');
|
|
34
33
|
const { createClientSessions, LEGACY_CLIENT_KEY } = require('./lib/feedback-remote/remoteClientSessions.cjs');
|
|
35
34
|
|
|
@@ -497,12 +496,11 @@ async function handleTokenRevoked(deviceId) {
|
|
|
497
496
|
sendIfAlive(_window, 'webRemote:token-revoked', { deviceId });
|
|
498
497
|
}
|
|
499
498
|
|
|
500
|
-
// ─── v2 mobile: session live state
|
|
499
|
+
// ─── v2 mobile: session live state push ────────────────────────────
|
|
501
500
|
//
|
|
502
501
|
// For each subscribed tab the agent tails its transcript JSONL (reusing the
|
|
503
502
|
// canonical classifyLine + transcriptPath from transcripts.cjs — single source of
|
|
504
503
|
// truth), derives a coarse state, and pushes event:session:state on change.
|
|
505
|
-
// The last completed assistant turn drives the Haiku summary (see maybeSummarize).
|
|
506
504
|
|
|
507
505
|
const SESSION_POLL_MS = 1500;
|
|
508
506
|
const SESSION_INIT_TAIL_BYTES = 512 * 1024; // bound the initial read
|
|
@@ -524,7 +522,7 @@ function pushEventTo(client, type, payload) {
|
|
|
524
522
|
}
|
|
525
523
|
}
|
|
526
524
|
|
|
527
|
-
/** Authenticated clients subscribed to a tab's session state
|
|
525
|
+
/** Authenticated clients subscribed to a tab's session state. */
|
|
528
526
|
function subscribersOf(tabId) {
|
|
529
527
|
return _clients.authenticated().filter((c) => c.subscriptions.has(tabId));
|
|
530
528
|
}
|
|
@@ -551,15 +549,6 @@ function teardownAllClients() {
|
|
|
551
549
|
return n;
|
|
552
550
|
}
|
|
553
551
|
|
|
554
|
-
/** Extract concatenated text from an assistant transcript line, or '' if none. */
|
|
555
|
-
function extractAssistantText(raw) {
|
|
556
|
-
const msg = raw?.message || raw;
|
|
557
|
-
const content = msg?.content;
|
|
558
|
-
if (typeof content === 'string') return content;
|
|
559
|
-
if (!Array.isArray(content)) return '';
|
|
560
|
-
return content.filter((b) => b?.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('\n').trim();
|
|
561
|
-
}
|
|
562
|
-
|
|
563
552
|
/** Map a classified transcript event to a coarse session state. */
|
|
564
553
|
function deriveState(ev, raw) {
|
|
565
554
|
// API/usage errors surface as a flagged message line.
|
|
@@ -613,6 +602,19 @@ async function tailLines(filePath, fromOffset) {
|
|
|
613
602
|
}
|
|
614
603
|
|
|
615
604
|
async function pollSessionWatcher(w) {
|
|
605
|
+
// The transcript may not exist yet (new session) or may live under another
|
|
606
|
+
// project/worktree encoding: re-resolve while the watched file is missing.
|
|
607
|
+
if (!fs.existsSync(w.filePath)) {
|
|
608
|
+
try {
|
|
609
|
+
const next = require('./transcripts.cjs').transcriptPath(w.cwd, w.sessionId);
|
|
610
|
+
if (next && next !== w.filePath) {
|
|
611
|
+
validatePath(next);
|
|
612
|
+
w.filePath = next;
|
|
613
|
+
w.offset = null;
|
|
614
|
+
w.inode = undefined;
|
|
615
|
+
}
|
|
616
|
+
} catch { /* keep the old path */ }
|
|
617
|
+
}
|
|
616
618
|
let res;
|
|
617
619
|
try {
|
|
618
620
|
res = await tailLines(w.filePath, w.offset);
|
|
@@ -627,8 +629,6 @@ async function pollSessionWatcher(w) {
|
|
|
627
629
|
w.inode = res.inode;
|
|
628
630
|
|
|
629
631
|
let nextState = null;
|
|
630
|
-
let newAssistantText = null;
|
|
631
|
-
let newMsgId = null;
|
|
632
632
|
for (const line of res.lines) {
|
|
633
633
|
let obj;
|
|
634
634
|
try { obj = JSON.parse(line); } catch { continue; }
|
|
@@ -636,10 +636,6 @@ async function pollSessionWatcher(w) {
|
|
|
636
636
|
for (const ev of events) {
|
|
637
637
|
const s = deriveState(ev, obj);
|
|
638
638
|
if (s) nextState = s;
|
|
639
|
-
if (ev.kind === 'assistant') {
|
|
640
|
-
const text = extractAssistantText(obj);
|
|
641
|
-
if (text) { newAssistantText = text; newMsgId = obj.uuid || obj.message?.id || `${w.tabId}:${res.size}`; }
|
|
642
|
-
}
|
|
643
639
|
}
|
|
644
640
|
}
|
|
645
641
|
|
|
@@ -650,33 +646,20 @@ async function pollSessionWatcher(w) {
|
|
|
650
646
|
pushEventTo(c, 'event:session:state', { tabId: w.tabId, state: w.state, since: Date.now() });
|
|
651
647
|
}
|
|
652
648
|
}
|
|
653
|
-
if (newAssistantText && newMsgId !== w.lastMsgId) {
|
|
654
|
-
w.lastAssistantText = newAssistantText;
|
|
655
|
-
w.lastMsgId = newMsgId;
|
|
656
|
-
}
|
|
657
|
-
// Summarize only a COMPLETED turn (state idle) — not assistant text mid-turn that
|
|
658
|
-
// is followed by a tool call. Cache by msgId so re-subscribe doesn't re-bill.
|
|
659
|
-
if (w.state === 'idle' && w.lastAssistantText && w.lastMsgId !== w.summarizedMsgId) {
|
|
660
|
-
w.summarizedMsgId = w.lastMsgId;
|
|
661
|
-
maybeSummarize(w).catch(() => {});
|
|
662
|
-
}
|
|
663
649
|
}
|
|
664
650
|
|
|
665
|
-
function startSessionWatch(tabId, cwd) {
|
|
651
|
+
function startSessionWatch(tabId, cwd, sessionId = tabId) {
|
|
666
652
|
if (_sessionWatchers.has(tabId)) return;
|
|
667
|
-
const filePath = require('./transcripts.cjs').transcriptPath(cwd,
|
|
653
|
+
const filePath = require('./transcripts.cjs').transcriptPath(cwd, sessionId);
|
|
668
654
|
// Defense in depth: the schema restricts tabId charset, but re-validate the
|
|
669
655
|
// FINAL joined path against the home-dir boundary (validatePath resolves
|
|
670
656
|
// symlinks + rejects escapes) before any fs read. Throws → dispatch drops it.
|
|
671
657
|
validatePath(filePath);
|
|
672
658
|
const w = {
|
|
673
|
-
tabId, cwd, filePath,
|
|
659
|
+
tabId, cwd, sessionId, filePath,
|
|
674
660
|
offset: null, // null → first poll reads a bounded tail then tracks EOF
|
|
675
661
|
inode: undefined,
|
|
676
662
|
state: 'idle',
|
|
677
|
-
lastAssistantText: null,
|
|
678
|
-
lastMsgId: null,
|
|
679
|
-
summarizedMsgId: null,
|
|
680
663
|
timer: null,
|
|
681
664
|
};
|
|
682
665
|
_sessionWatchers.set(tabId, w);
|
|
@@ -732,44 +715,6 @@ function buildRemoteSessionList(tabs, readIndex) {
|
|
|
732
715
|
return out;
|
|
733
716
|
}
|
|
734
717
|
|
|
735
|
-
// ─── SM-V2-03: mobile summary via Claude Haiku 4.5 ───────────────────────────
|
|
736
|
-
// Summarization logic lives in lib/summarize.cjs (shared with exchanges.cjs).
|
|
737
|
-
|
|
738
|
-
const SUMMARY_MIN_CHARS = 280; // below this, push raw — not worth an API call
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Produce a mobile summary of the watcher's last completed assistant turn and push it.
|
|
742
|
-
* Short turns are pushed raw (no API call). If no API key is configured, degrades to
|
|
743
|
-
* the raw message so core remote control is never blocked. Cost: Haiku in+out per
|
|
744
|
-
* completed turn per subscribed tab (~$1/$5 per 1M tokens).
|
|
745
|
-
*/
|
|
746
|
-
async function maybeSummarize(w) {
|
|
747
|
-
// Guard: summaries (session transcript content) go only to SAS-confirmed subscribers.
|
|
748
|
-
if (subscribersOf(w.tabId).length === 0) return;
|
|
749
|
-
const text = w.lastAssistantText;
|
|
750
|
-
if (!text) return;
|
|
751
|
-
const ofMessageId = w.lastMsgId;
|
|
752
|
-
|
|
753
|
-
if (text.length < SUMMARY_MIN_CHARS) {
|
|
754
|
-
for (const c of subscribersOf(w.tabId)) {
|
|
755
|
-
pushEventTo(c, 'event:session:summary', { tabId: w.tabId, summary: text, ofMessageId, model: 'raw', ts: Date.now() });
|
|
756
|
-
}
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const { summary, model, degraded } = await _sharedSummarize(text);
|
|
761
|
-
if (degraded) {
|
|
762
|
-
logs.writeLine({ scope: 'webRemote', level: 'warn', message: `summary degraded: ${degraded}`, meta: {} });
|
|
763
|
-
}
|
|
764
|
-
for (const c of subscribersOf(w.tabId)) {
|
|
765
|
-
pushEventTo(c, 'event:session:summary', {
|
|
766
|
-
tabId: w.tabId, summary, ofMessageId, model,
|
|
767
|
-
...(degraded ? { degraded } : {}),
|
|
768
|
-
ts: Date.now(),
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
718
|
/**
|
|
774
719
|
* Shared finish-up for any path that transitions a client's e2e state into 'authenticated'
|
|
775
720
|
* (manual SAS confirmation or pinned-key auto-trust): log, broadcast the new
|
|
@@ -1010,7 +955,7 @@ async function dispatchEnvelope(envelope, device, client) {
|
|
|
1010
955
|
let result;
|
|
1011
956
|
try {
|
|
1012
957
|
result = await dispatchCommand(type, payload ?? {}, client);
|
|
1013
|
-
await auditLog(ts, type, device.deviceId, id,
|
|
958
|
+
await auditLog(ts, type, device.deviceId, id, auditResultFor(result), client);
|
|
1014
959
|
} catch (e) {
|
|
1015
960
|
const code = e?.name === 'ZodError' ? 'schema_invalid' : 'dispatch_error';
|
|
1016
961
|
await auditLog(ts, type, device.deviceId, id, `error:${code}`, client);
|
|
@@ -1020,6 +965,14 @@ async function dispatchEnvelope(envelope, device, client) {
|
|
|
1020
965
|
respond(id, result, undefined, client);
|
|
1021
966
|
}
|
|
1022
967
|
|
|
968
|
+
/** Audit result string for a handler result: structured refusals log as refused[:reason]. */
|
|
969
|
+
function auditResultFor(result) {
|
|
970
|
+
if (result && result.ok === false && typeof result.disposition === 'string') {
|
|
971
|
+
return typeof result.reason === 'string' && result.reason ? `refused:${result.reason}` : 'refused';
|
|
972
|
+
}
|
|
973
|
+
return 'ok';
|
|
974
|
+
}
|
|
975
|
+
|
|
1023
976
|
function respond(msgId, payload, typeOverride, client) {
|
|
1024
977
|
if (!_ws || _ws.readyState !== WebSocket.OPEN) return;
|
|
1025
978
|
const responseType = typeOverride || (msgId ? `resp:${msgId}` : undefined);
|
|
@@ -1102,26 +1055,34 @@ function getDispatchMap() {
|
|
|
1102
1055
|
const parsed = schemas.remoteSessionTranscript.parse(payload);
|
|
1103
1056
|
const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
|
|
1104
1057
|
const { resolveEpicTranscriptPath } = require('./lib/epic/epicTranscriptPath.cjs');
|
|
1105
|
-
const { readRecentChatMessages } = require('./lib/remoteTranscriptTail.cjs');
|
|
1058
|
+
const { readRecentChatMessages, buildRemoteConversation } = require('./lib/remoteTranscriptTail.cjs');
|
|
1059
|
+
const { listExchanges } = require('./exchanges.cjs');
|
|
1106
1060
|
const data = await sessionsStore.load();
|
|
1107
1061
|
const tab = (data?.tabs ?? []).find((t) => t && t.cwd === parsed.cwd);
|
|
1108
1062
|
if (!tab) throw new Error('cwd is not an open project');
|
|
1109
1063
|
validatePath(parsed.cwd);
|
|
1064
|
+
const limit = parsed.limit ?? 10;
|
|
1065
|
+
const before = parsed.before ?? 0;
|
|
1110
1066
|
let filePath = null;
|
|
1111
|
-
|
|
1112
|
-
|
|
1067
|
+
let sessionId = null;
|
|
1068
|
+
if (parsed.sessionKey === tab.id || parsed.sessionKey === tab.sessionId) {
|
|
1069
|
+
sessionId = tab.sessionId || parsed.sessionKey;
|
|
1070
|
+
filePath = require('./transcripts.cjs').transcriptPath(parsed.cwd, sessionId);
|
|
1113
1071
|
} else {
|
|
1114
1072
|
const sessions = readActiveIndex(parsed.cwd)?.sessions ?? {};
|
|
1115
1073
|
const epic = Object.values(sessions).find(
|
|
1116
1074
|
(e) => e && (e.id === parsed.sessionKey || e.claudeSessionId === parsed.sessionKey),
|
|
1117
1075
|
);
|
|
1118
1076
|
if (epic && epic.claudeSessionId) {
|
|
1119
|
-
|
|
1077
|
+
sessionId = epic.claudeSessionId;
|
|
1078
|
+
filePath = resolveEpicTranscriptPath({ cwd: parsed.cwd, claudeSessionId: sessionId }).path;
|
|
1120
1079
|
}
|
|
1121
1080
|
}
|
|
1122
|
-
if (!filePath) return { messages: [] };
|
|
1081
|
+
if (!filePath || !sessionId) return { messages: [], hasMore: false, nextBefore: null };
|
|
1123
1082
|
validatePath(filePath);
|
|
1124
|
-
|
|
1083
|
+
const exchanges = await listExchanges({ cwd: parsed.cwd, sessionId, limit: limit + 1, offset: before });
|
|
1084
|
+
const tail = before === 0 ? await readRecentChatMessages(filePath, { limit: 30 }) : [];
|
|
1085
|
+
return buildRemoteConversation({ exchanges, tail, limit, before });
|
|
1125
1086
|
},
|
|
1126
1087
|
|
|
1127
1088
|
'cmd:session:prds': async (payload) => {
|
|
@@ -1209,12 +1170,15 @@ function getDispatchMap() {
|
|
|
1209
1170
|
'cmd:app:info': async () =>
|
|
1210
1171
|
({ version: app.getVersion(), platform: process.platform }),
|
|
1211
1172
|
|
|
1212
|
-
// v2 mobile: start/stop pushing live state
|
|
1173
|
+
// v2 mobile: start/stop pushing live state for a session.
|
|
1213
1174
|
'cmd:session:subscribe': async (payload, reqClient) => {
|
|
1214
1175
|
const client = reqClient || _clients.getOrCreate(LEGACY_CLIENT_KEY);
|
|
1215
1176
|
const parsed = schemas.sessionSubscribe.parse(payload);
|
|
1216
1177
|
validatePath(parsed.cwd); // home-dir boundary before any fs access
|
|
1217
|
-
|
|
1178
|
+
// The phone's Main row passes the tab `id`, but the transcript is keyed by the tab's `sessionId`.
|
|
1179
|
+
const { tabs } = await require('./sessionsStore.cjs').load();
|
|
1180
|
+
const tab = (Array.isArray(tabs) ? tabs : []).find((t) => t && t.id === parsed.tabId && t.cwd === parsed.cwd);
|
|
1181
|
+
startSessionWatch(parsed.tabId, parsed.cwd, tab?.sessionId || parsed.tabId);
|
|
1218
1182
|
client.subscriptions.add(parsed.tabId);
|
|
1219
1183
|
return { ok: true };
|
|
1220
1184
|
},
|
|
@@ -1236,23 +1200,28 @@ function getDispatchMap() {
|
|
|
1236
1200
|
const data = await sessionsStore.load();
|
|
1237
1201
|
const tabs = data?.tabs ?? [];
|
|
1238
1202
|
// Resolve the target in main: an open tab, or an ACTIVE Epic (by id or
|
|
1239
|
-
// claudeSessionId) of an open project. Anything else
|
|
1203
|
+
// claudeSessionId) of an open project. Anything else is a structured refusal.
|
|
1204
|
+
const outcome = (r) => ({
|
|
1205
|
+
ok: r.disposition !== 'refused',
|
|
1206
|
+
disposition: r.disposition,
|
|
1207
|
+
...(r.reason ? { reason: r.reason } : {}),
|
|
1208
|
+
});
|
|
1240
1209
|
if (tabs.some((t) => t.sessionId === parsed.tabId || t.id === parsed.tabId)) {
|
|
1241
|
-
chatRunner.enqueueExternalPrompt(parsed.tabId, parsed.prompt);
|
|
1242
|
-
return { ok: true };
|
|
1210
|
+
return outcome(await chatRunner.enqueueExternalPrompt(parsed.tabId, parsed.prompt));
|
|
1243
1211
|
}
|
|
1244
1212
|
for (const tab of tabs) {
|
|
1245
1213
|
const sessions = Object.values(readActiveIndex(tab.cwd)?.sessions ?? {});
|
|
1246
1214
|
const epic = sessions.find((e) => e && (e.id === parsed.tabId || e.claudeSessionId === parsed.tabId));
|
|
1247
1215
|
if (!epic) continue;
|
|
1248
|
-
if (epic.status !== 'active' || !epic.claudeSessionId)
|
|
1249
|
-
|
|
1216
|
+
if (epic.status !== 'active' || !epic.claudeSessionId) {
|
|
1217
|
+
return { ok: false, disposition: 'refused', reason: epic.status === 'proposed' ? 'proposed' : 'not-active' };
|
|
1218
|
+
}
|
|
1219
|
+
return outcome(await chatRunner.enqueueExternalPrompt(epic.id, parsed.prompt, {
|
|
1250
1220
|
sessionId: epic.claudeSessionId,
|
|
1251
1221
|
cwd: epic.cwd || tab.cwd,
|
|
1252
|
-
});
|
|
1253
|
-
return { ok: true };
|
|
1222
|
+
}));
|
|
1254
1223
|
}
|
|
1255
|
-
|
|
1224
|
+
return { ok: false, disposition: 'refused', reason: 'unknown-target' };
|
|
1256
1225
|
},
|
|
1257
1226
|
|
|
1258
1227
|
'cmd:session:options': async () => {
|
|
@@ -1266,6 +1235,7 @@ function getDispatchMap() {
|
|
|
1266
1235
|
agents: personas.map((a) => ({ name: a.name, description: a.description })),
|
|
1267
1236
|
tags: EpicTagSchema.options,
|
|
1268
1237
|
macros: await macroLibrary.listMacros(),
|
|
1238
|
+
macroInputs: true,
|
|
1269
1239
|
};
|
|
1270
1240
|
},
|
|
1271
1241
|
|
|
@@ -1278,7 +1248,23 @@ function getDispatchMap() {
|
|
|
1278
1248
|
if (!tabs.some((t) => t.cwd === parsed.cwd)) throw new Error('cwd is not an open project');
|
|
1279
1249
|
const personas = await agentLibrary.listPersonas();
|
|
1280
1250
|
if (!personas.some((a) => a.name === parsed.agentName)) throw new Error('unknown agent');
|
|
1281
|
-
|
|
1251
|
+
let prompt = parsed.prompt;
|
|
1252
|
+
if (parsed.macroId) {
|
|
1253
|
+
const macroLibrary = require('./lib/persona-macro/macroLibrary.cjs');
|
|
1254
|
+
const macros = await macroLibrary.listMacros();
|
|
1255
|
+
const macro = macros.find((m) => m.id === parsed.macroId);
|
|
1256
|
+
if (!macro || !macroLibrary.macroVisibleIn(macro, parsed.cwd)) throw new Error('unknown macro');
|
|
1257
|
+
const result = macroLibrary.renderMacroPrompt(parsed.prompt, macro.inputs, parsed.inputs ?? {});
|
|
1258
|
+
if (!result.ok) throw new Error(result.error);
|
|
1259
|
+
prompt = result.prompt;
|
|
1260
|
+
}
|
|
1261
|
+
chatRunner.requestExternalNewSession({
|
|
1262
|
+
cwd: parsed.cwd,
|
|
1263
|
+
agentName: parsed.agentName,
|
|
1264
|
+
tag: parsed.tag,
|
|
1265
|
+
title: parsed.title,
|
|
1266
|
+
prompt,
|
|
1267
|
+
});
|
|
1282
1268
|
return { ok: true };
|
|
1283
1269
|
},
|
|
1284
1270
|
};
|
|
@@ -1551,6 +1537,11 @@ module.exports = {
|
|
|
1551
1537
|
},
|
|
1552
1538
|
__test: {
|
|
1553
1539
|
getDispatchMap,
|
|
1540
|
+
auditResultFor,
|
|
1541
|
+
startSessionWatch,
|
|
1542
|
+
pollSessionWatcher,
|
|
1543
|
+
getSessionWatcher: (tabId) => _sessionWatchers.get(tabId),
|
|
1544
|
+
stopAllSessionWatches,
|
|
1554
1545
|
buildRemoteSessionList,
|
|
1555
1546
|
setWs: (ws) => { _ws = ws; },
|
|
1556
1547
|
connect,
|