claude-code-session-manager 0.27.0 → 0.29.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/.claude-plugin/marketplace.json +21 -0
- package/dist/assets/{TiptapBody-BsNr6F0B.js → TiptapBody-DyiZVN0v.js} +1 -1
- package/dist/assets/index-DMIi9YZH.css +32 -0
- package/dist/assets/{index-DU1pkhIQ.js → index-qS7GdCsL.js} +391 -389
- package/dist/index.html +2 -2
- package/package.json +3 -1
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +19 -0
- package/plugins/session-manager-dev/skills/develop/SKILL.md +112 -0
- package/plugins/session-manager-dev/skills/develop/standards.md +67 -0
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +192 -0
- package/plugins/session-manager-dev/skills/explain-to-me/assets/style-reference.html +163 -0
- package/plugins/session-manager-dev/skills/local-project-health/SKILL.md +58 -0
- package/plugins/session-manager-dev/skills/my-feedback/SKILL.md +132 -0
- package/plugins/session-manager-dev/skills/optimize-kpi/SKILL.md +289 -0
- package/plugins/session-manager-dev/skills/prd/SKILL.md +134 -0
- package/plugins/session-manager-dev/skills/process-feedback/SKILL.md +183 -0
- package/plugins/session-manager-dev/skills/project-status/SKILL.md +244 -0
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +105 -0
- package/plugins/session-manager-dev/skills/requesting-code-review/code-reviewer.md +146 -0
- package/plugins/session-manager-dev/skills/security-review/SKILL.md +105 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +95 -0
- package/src/main/__tests__/scheduler-autopromote.test.cjs +33 -0
- package/src/main/index.cjs +7 -0
- package/src/main/ipcSchemas.cjs +10 -0
- package/src/main/lib/credentials.cjs +96 -11
- package/src/main/pluginInstall.cjs +114 -32
- package/src/main/scheduler.cjs +109 -5
- package/src/main/seedDevPlugin.cjs +95 -0
- package/src/main/templates/PRD_AUTHORING.md +316 -0
- package/src/main/usage.cjs +34 -1
- package/src/preload/api.d.ts +8 -2
- package/dist/assets/index-Dwb94Uxm.css +0 -32
package/src/main/scheduler.cjs
CHANGED
|
@@ -337,9 +337,21 @@ function safeSlugPath(slug) {
|
|
|
337
337
|
return resolved;
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
// Bundled authoring guide seeded into the scheduler dir so the session-manager-dev
|
|
341
|
+
// plugin's /develop and /prd skills — which reference this stable `~`-absolute
|
|
342
|
+
// path — work on any user's machine, not just the author's.
|
|
343
|
+
const PRD_AUTHORING_TEMPLATE = path.join(__dirname, 'templates', 'PRD_AUTHORING.md');
|
|
344
|
+
const PRD_AUTHORING_DEST = path.join(ROOT, 'PRD_AUTHORING.md');
|
|
345
|
+
|
|
340
346
|
function ensureDirs() {
|
|
341
347
|
fs.mkdirSync(PRDS_DIR, { recursive: true });
|
|
342
348
|
fs.mkdirSync(RUNS_DIR, { recursive: true });
|
|
349
|
+
// Seed the authoring guide once; never clobber a user's edited copy.
|
|
350
|
+
try {
|
|
351
|
+
if (!fs.existsSync(PRD_AUTHORING_DEST) && fs.existsSync(PRD_AUTHORING_TEMPLATE)) {
|
|
352
|
+
fs.copyFileSync(PRD_AUTHORING_TEMPLATE, PRD_AUTHORING_DEST);
|
|
353
|
+
}
|
|
354
|
+
} catch { /* non-fatal: the guide is a convenience, not load-bearing for a run */ }
|
|
343
355
|
}
|
|
344
356
|
|
|
345
357
|
// Atomic JSON write helpers delegate to config.cjs's shared implementation.
|
|
@@ -1104,6 +1116,15 @@ function isFixPlanSlug(slug) {
|
|
|
1104
1116
|
return /^\d+-fix-/.test(slug);
|
|
1105
1117
|
}
|
|
1106
1118
|
|
|
1119
|
+
/**
|
|
1120
|
+
* Returns true for statuses that a fix-plan completion should promote
|
|
1121
|
+
* (clear) on the original job. Both 'failed' and 'needs_review' are
|
|
1122
|
+
* recoverable via a fix-plan; 'completed', 'running', 'pending' are not.
|
|
1123
|
+
*/
|
|
1124
|
+
function isPromotableOriginal(status) {
|
|
1125
|
+
return status === 'failed' || status === 'needs_review';
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1107
1128
|
/**
|
|
1108
1129
|
* Spawn an Opus investigation session for a failed job. The investigator's job
|
|
1109
1130
|
* is to read the failure log + original PRD, identify the root cause, and write
|
|
@@ -1446,13 +1467,17 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
1446
1467
|
// though the auto-recovery did its job.
|
|
1447
1468
|
if (effectiveStatus === 'completed' && isFixPlanSlug(job.slug)) {
|
|
1448
1469
|
const originalSlug = job.slug.replace(/^(\d+)-fix-/, '$1-');
|
|
1449
|
-
const orig = s.jobs.findIndex((x) => x.slug === originalSlug && x.status
|
|
1470
|
+
const orig = s.jobs.findIndex((x) => x.slug === originalSlug && isPromotableOriginal(x.status));
|
|
1450
1471
|
if (orig >= 0) {
|
|
1451
|
-
|
|
1472
|
+
const priorStatus = s.jobs[orig].status;
|
|
1473
|
+
console.log(`[scheduler] auto-promote: ${originalSlug} (${priorStatus}) → completed because ${job.slug} succeeded`);
|
|
1452
1474
|
s.jobs[orig].status = 'completed';
|
|
1453
1475
|
s.jobs[orig].exitCode = 0;
|
|
1454
1476
|
s.jobs[orig].error = null;
|
|
1455
1477
|
s.jobs[orig].completedBy = job.slug;
|
|
1478
|
+
if (priorStatus === 'needs_review') {
|
|
1479
|
+
delete s.jobs[orig].verifierVerdict;
|
|
1480
|
+
}
|
|
1456
1481
|
}
|
|
1457
1482
|
}
|
|
1458
1483
|
}
|
|
@@ -1660,6 +1685,34 @@ function pollRecoveryClearSource(pauseReason, hasCachedReset) {
|
|
|
1660
1685
|
async function pollLoop() {
|
|
1661
1686
|
try {
|
|
1662
1687
|
await reapDeadRunningJobs().catch(() => {});
|
|
1688
|
+
|
|
1689
|
+
// Enterprise auth (Bedrock / Vertex / API-key / corporate gateway): there is
|
|
1690
|
+
// no consumer 5-hour usage meter to poll. Don't hit an endpoint that will
|
|
1691
|
+
// 404/time-out and eventually pause the queue on 'network' — treat usage as
|
|
1692
|
+
// wide-open and fire on pending + memory alone. (Blackrock-style machines.)
|
|
1693
|
+
if (!billing.usageMeterApplicable()) {
|
|
1694
|
+
cachedUtilization = 0;
|
|
1695
|
+
consecutiveFailures = 0;
|
|
1696
|
+
backoffMs = 0;
|
|
1697
|
+
backoffNextAt = null;
|
|
1698
|
+
firstFailureAt = null;
|
|
1699
|
+
firstNon429FailureAt = null;
|
|
1700
|
+
lastFailureKind = null;
|
|
1701
|
+
lastPollAt = Date.now();
|
|
1702
|
+
lastPollOk = true;
|
|
1703
|
+
persistSchedulerState();
|
|
1704
|
+
let cur = await readQueue();
|
|
1705
|
+
// Clear a stale auth/network pause inherited from a prior consumer-auth
|
|
1706
|
+
// session so the queue isn't wedged. Never clears rate_limit/manual.
|
|
1707
|
+
if (cur.paused && (cur.paused.reason === 'auth' || cur.paused.reason === 'network')) {
|
|
1708
|
+
await clearPause('enterprise-auth');
|
|
1709
|
+
cur = await readQueue(); // re-read so the cleared pause is visible to launch THIS cycle
|
|
1710
|
+
}
|
|
1711
|
+
await maybeLaunchWhenAvailable(cur);
|
|
1712
|
+
await broadcast();
|
|
1713
|
+
return; // finally re-arms the timer
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1663
1716
|
const r = await billing.fetchUsage();
|
|
1664
1717
|
|
|
1665
1718
|
if (r.kind === 'ok') {
|
|
@@ -1676,9 +1729,12 @@ async function pollLoop() {
|
|
|
1676
1729
|
persistSchedulerState();
|
|
1677
1730
|
|
|
1678
1731
|
// Clear any pause that was waiting for a successful billing read.
|
|
1679
|
-
|
|
1732
|
+
let cur = await readQueue();
|
|
1680
1733
|
const clearSrc = pollRecoveryClearSource(cur.paused?.reason ?? null, !!cachedNextReset);
|
|
1681
|
-
if (clearSrc)
|
|
1734
|
+
if (clearSrc) {
|
|
1735
|
+
await clearPause(clearSrc);
|
|
1736
|
+
cur = await readQueue(); // re-read so the cleared pause launches work THIS cycle
|
|
1737
|
+
}
|
|
1682
1738
|
|
|
1683
1739
|
await maybeLaunchWhenAvailable(cur);
|
|
1684
1740
|
await broadcast();
|
|
@@ -1790,6 +1846,31 @@ function isRescanCandidate(job) {
|
|
|
1790
1846
|
*
|
|
1791
1847
|
* @returns {Promise<{rescanned:number, healed:string[]}>}
|
|
1792
1848
|
*/
|
|
1849
|
+
/**
|
|
1850
|
+
* Pure helper — no I/O. Returns the subset of jobs eligible for automatic
|
|
1851
|
+
* fix-plan authoring after a reverify pass leaves them still in needs_review.
|
|
1852
|
+
*
|
|
1853
|
+
* Exclusion rules (all must pass):
|
|
1854
|
+
* - status === 'needs_review'
|
|
1855
|
+
* - truthy runId (need a run log to investigate)
|
|
1856
|
+
* - autoFixAttempted !== true (1-attempt cap)
|
|
1857
|
+
* - not itself a fix-plan slug (avoids infinite recursion)
|
|
1858
|
+
* - no fix sibling on disk (fixSlugExists) or already in the queue
|
|
1859
|
+
*/
|
|
1860
|
+
function selectAutoFixTargets(jobs, { fixSlugExists }) {
|
|
1861
|
+
const slugsInQueue = new Set(jobs.map((j) => j.slug));
|
|
1862
|
+
return jobs.filter((job) => {
|
|
1863
|
+
if (job.status !== 'needs_review') return false;
|
|
1864
|
+
if (!job.runId) return false;
|
|
1865
|
+
if (job.autoFixAttempted) return false;
|
|
1866
|
+
if (isFixPlanSlug(job.slug)) return false;
|
|
1867
|
+
const fixSlug = `${String(job.parallelGroup ?? 99).padStart(2, '0')}-fix-${job.slug.replace(/^\d+-/, '')}`;
|
|
1868
|
+
if (fixSlugExists(fixSlug)) return false;
|
|
1869
|
+
if (slugsInQueue.has(fixSlug)) return false;
|
|
1870
|
+
return true;
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1793
1874
|
async function reverifyNeedsReview() {
|
|
1794
1875
|
const snap = await readQueue();
|
|
1795
1876
|
const candidates = snap.jobs.filter(isRescanCandidate);
|
|
@@ -1837,6 +1918,29 @@ async function reverifyNeedsReview() {
|
|
|
1837
1918
|
const detail = leftForReview.map((e) => `${e.slug} (${e.reason})`).join(', ');
|
|
1838
1919
|
console.log(`[scheduler] boot reverify: left for review: ${detail}`);
|
|
1839
1920
|
}
|
|
1921
|
+
|
|
1922
|
+
// Auto-fix: spawn a fix-plan investigation for each job still in
|
|
1923
|
+
// needs_review after the heal pass (kill-switch: SM_AUTOFIX_DISABLE=1).
|
|
1924
|
+
if (process.env.SM_AUTOFIX_DISABLE !== '1') {
|
|
1925
|
+
const afterHeal = await readQueue();
|
|
1926
|
+
const targets = selectAutoFixTargets(afterHeal.jobs, {
|
|
1927
|
+
fixSlugExists: (s) => fs.existsSync(path.join(PRDS_DIR, `${s}.md`)),
|
|
1928
|
+
});
|
|
1929
|
+
for (const job of targets) {
|
|
1930
|
+
const runDir = path.join(RUNS_DIR, job.runId);
|
|
1931
|
+
// Persist cap BEFORE spawning — a crash mid-investigation still counts
|
|
1932
|
+
// the attempt (mirrors orphanRetries pattern).
|
|
1933
|
+
await mutate((s) => {
|
|
1934
|
+
const j = s.jobs.find((x) => x.slug === job.slug);
|
|
1935
|
+
if (j) j.autoFixAttempted = true;
|
|
1936
|
+
});
|
|
1937
|
+
console.log(`[scheduler] auto-fix: needs_review ${job.slug} → authoring fix-plan (1/1)`);
|
|
1938
|
+
spawnInvestigation(job, runDir).catch((e) => {
|
|
1939
|
+
console.error('[scheduler] auto-fix spawnInvestigation error', job.slug, e);
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1840
1944
|
return { rescanned: candidates.length, healed, leftForReview };
|
|
1841
1945
|
}
|
|
1842
1946
|
|
|
@@ -2353,4 +2457,4 @@ const remote = {
|
|
|
2353
2457
|
},
|
|
2354
2458
|
};
|
|
2355
2459
|
|
|
2356
|
-
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate };
|
|
2460
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-boot seeder for the bundled `session-manager-dev` plugin.
|
|
3
|
+
*
|
|
4
|
+
* The plugin (its 10 dev skills) ships inside the npx distribution. To make it
|
|
5
|
+
* a true default, the app installs it on first launch from its own bundled
|
|
6
|
+
* marketplace (offline — no GitHub/registry). Idempotent two ways:
|
|
7
|
+
*
|
|
8
|
+
* 1. A marker file (`~/.claude/session-manager/.dev-plugin-seeded`) records
|
|
9
|
+
* seed state. Once the install SUCCEEDS we write `done` and never touch it
|
|
10
|
+
* again, so a deliberate uninstall stays uninstalled. A FAILED attempt only
|
|
11
|
+
* bumps an attempt counter and retries on the next few boots — a transient
|
|
12
|
+
* first-boot failure (e.g. `claude` not yet on PATH, briefly offline) must
|
|
13
|
+
* not permanently skip the default plugin. After MAX_ATTEMPTS we give up
|
|
14
|
+
* (manual install via the Plugins library button is always available).
|
|
15
|
+
* 2. Before attempting, we check settings.json `enabledPlugins` — if the
|
|
16
|
+
* plugin is already enabled (e.g. installed by hand), we just write the
|
|
17
|
+
* marker and skip the install.
|
|
18
|
+
*
|
|
19
|
+
* Fire-and-forget: called post-window from index.cjs. Errors are logged, never
|
|
20
|
+
* thrown. Kill-switch: SM_SEED_DEV_PLUGIN_DISABLE=1.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const os = require('node:os');
|
|
26
|
+
const { install } = require('./pluginInstall.cjs');
|
|
27
|
+
|
|
28
|
+
const PLUGIN_SLUG = 'session-manager-dev';
|
|
29
|
+
const MARKETPLACE = { add: 'bundled', name: 'session-manager' };
|
|
30
|
+
const ENABLED_KEY = `${PLUGIN_SLUG}@${MARKETPLACE.name}`;
|
|
31
|
+
const MAX_ATTEMPTS = 3; // give a transient first-boot failure a few chances
|
|
32
|
+
|
|
33
|
+
function markerPath() {
|
|
34
|
+
return path.join(os.homedir(), '.claude', 'session-manager', '.dev-plugin-seeded');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Read the marker → { done:boolean, attempts:number }. Absent = fresh. */
|
|
38
|
+
function readMarker() {
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(markerPath(), 'utf8').trim();
|
|
41
|
+
const m = JSON.parse(raw);
|
|
42
|
+
return { done: !!m.done, attempts: Number(m.attempts) || 0 };
|
|
43
|
+
} catch {
|
|
44
|
+
return { done: false, attempts: 0 };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function alreadyEnabled() {
|
|
49
|
+
try {
|
|
50
|
+
const raw = fs.readFileSync(path.join(os.homedir(), '.claude', 'settings.json'), 'utf8');
|
|
51
|
+
const enabled = JSON.parse(raw)?.enabledPlugins;
|
|
52
|
+
return !!(enabled && enabled[ENABLED_KEY]);
|
|
53
|
+
} catch {
|
|
54
|
+
return false; // settings absent/unparseable — treat as not-enabled.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeMarker(state) {
|
|
59
|
+
try {
|
|
60
|
+
const p = markerPath();
|
|
61
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
62
|
+
fs.writeFileSync(p, JSON.stringify({ ...state, ts: new Date().toISOString() }) + '\n');
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.warn('[seedDevPlugin] could not write marker:', err?.message ?? err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function seedDevPlugin({ logger = console } = {}) {
|
|
69
|
+
if (process.env.SM_SEED_DEV_PLUGIN_DISABLE === '1') return;
|
|
70
|
+
const marker = readMarker();
|
|
71
|
+
if (marker.done) return; // succeeded before — leave it alone.
|
|
72
|
+
if (marker.attempts >= MAX_ATTEMPTS) return; // gave up — manual install only.
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (alreadyEnabled()) {
|
|
76
|
+
writeMarker({ done: true, attempts: marker.attempts });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
logger.log?.('[seedDevPlugin] installing bundled session-manager-dev plugin…');
|
|
80
|
+
const r = await install({ slug: PLUGIN_SLUG, marketplace: MARKETPLACE });
|
|
81
|
+
if (r.ok) {
|
|
82
|
+
logger.log?.('[seedDevPlugin] installed session-manager-dev@session-manager');
|
|
83
|
+
writeMarker({ done: true, attempts: marker.attempts });
|
|
84
|
+
} else {
|
|
85
|
+
// Failure: bump the attempt counter so the next boot can retry (bounded).
|
|
86
|
+
logger.warn?.(`[seedDevPlugin] install failed (exit ${r.exitCode})${r.error ? ` — ${r.error}` : ''}; attempt ${marker.attempts + 1}/${MAX_ATTEMPTS}`);
|
|
87
|
+
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
logger.warn?.('[seedDevPlugin] error:', err?.message ?? err);
|
|
91
|
+
writeMarker({ done: false, attempts: marker.attempts + 1 });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { seedDevPlugin };
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# PRD Authoring Guide — Scheduler Safety Rules
|
|
2
|
+
|
|
3
|
+
This guide codifies lessons from two stuck-job incidents (fizzpop poll-hang, etch-engine post-AC overrun) into enforceable rules every PRD MUST follow. Violating these rules costs real money and wastes hours waiting at a terminal.
|
|
4
|
+
|
|
5
|
+
Before queueing a new PRD, run through the §10 checklist at the bottom.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## §1 Bounded waits, never unbounded polls
|
|
10
|
+
|
|
11
|
+
**Summary:** Every `until`/`while` loop that makes network calls or waits for external state MUST have a hard iteration cap that surfaces non-zero on exhaustion.
|
|
12
|
+
|
|
13
|
+
**Anti-example** (verbatim from `106-fizzpop-publish.md`):
|
|
14
|
+
```bash
|
|
15
|
+
# WRONG — what 106-fizzpop-publish did
|
|
16
|
+
PREV=288694
|
|
17
|
+
until [ "$(curl -s https://bilko.run/api/health | jq .uptime)" -lt "$PREV" ]; do
|
|
18
|
+
sleep 15
|
|
19
|
+
done
|
|
20
|
+
# Outcome: 2h47m hang because static-content Render deploys never restart the Node API
|
|
21
|
+
# so `uptime` never dropped. Loop hung until the 4h watchdog SIGKILLed.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Recommended pattern:**
|
|
25
|
+
```bash
|
|
26
|
+
# RIGHT — bounded, surfaces failure cleanly
|
|
27
|
+
for i in $(seq 1 20); do
|
|
28
|
+
if curl -sf https://bilko.run/projects/fizzpop/ > /dev/null; then
|
|
29
|
+
echo "deploy live (attempt $i)"; break
|
|
30
|
+
fi
|
|
31
|
+
echo "waiting for deploy ($i/20)..."
|
|
32
|
+
sleep 15
|
|
33
|
+
done
|
|
34
|
+
# Smoke test downstream will diagnose the real failure if the deploy didn't land.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Rule:** Every `until`/`while` network-or-state poll MUST use `for i in $(seq 1 N); do ...; done` with a hard cap. Recommended cap: 20 × 15 s = 5 min for HTTP polls. On exhaustion, print a diagnostic line and continue (let the smoke test below catch the real failure).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## §2 Don't add work past the acceptance checklist
|
|
42
|
+
|
|
43
|
+
**Summary:** Once every AC line is ticked, write the result and exit. Do not add polish, fixtures, generators, or "while we're here" improvements not enumerated in the PRD.
|
|
44
|
+
|
|
45
|
+
**Anti-example** (verbatim from `112-etch-engine.md`):
|
|
46
|
+
```bash
|
|
47
|
+
# WRONG — what 112-etch-engine did after declaring success
|
|
48
|
+
for (let seed = 100; seed < 50000 && !found; seed++) {
|
|
49
|
+
const result = generateRandom({ size, rng, maxAttempts: 3 });
|
|
50
|
+
if (result.ok && result.solution) { found = true; ... }
|
|
51
|
+
}
|
|
52
|
+
# Outcome: 2h44m of token burn looking for fixtures the AC did not require.
|
|
53
|
+
# The agent had emitted result-success at 17:44 UTC; the bonus loop ran until 20:28.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Rule:** Once every AC line is checked, write the result and exit 0. If bonus work seems genuinely valuable, write a follow-up PRD and reference it in the result. Do not add any work not explicitly enumerated in an AC line.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## §3 Smoke tests verify; spin-waits don't
|
|
61
|
+
|
|
62
|
+
**Summary:** Prefer "do thing, run a test that asserts thing happened" over "do thing, spin until I detect thing happened."
|
|
63
|
+
|
|
64
|
+
**Rule:** After a deployment, migration, or build step, run an actual test command (`curl -sf`, `npm test`, `pnpm typecheck`) that exits non-zero on failure. A spin-wait that polls for a condition hides the failure mode; a test command surfaces the exact error.
|
|
65
|
+
|
|
66
|
+
**Pattern:**
|
|
67
|
+
```bash
|
|
68
|
+
# Deploy step above (bounded, §1)
|
|
69
|
+
# Smoke test — will exit 1 with a clear message if deploy failed:
|
|
70
|
+
curl -sf https://bilko.run/projects/fizzpop/ > /dev/null || { echo "smoke test FAILED: fizzpop not reachable"; exit 1; }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## §4 Bound any generator/search loop with max-attempts and surface-on-exhaustion
|
|
76
|
+
|
|
77
|
+
**Summary:** When iterating a search space (fixtures, seeds, brute-force), declare the maximum search size in the PRD AC and surface and HALT on exhaustion.
|
|
78
|
+
|
|
79
|
+
**Anti-example:** Same etch-engine fixture generator from §2 — `for (let seed = 100; seed < 50000 ...)` with no AC line constraining it.
|
|
80
|
+
|
|
81
|
+
**Rule:** When iterating a search space, the PRD AC MUST state the bound explicitly ("up to 1000 seeds; if exhausted, surface and HALT"). The executor knows when to give up and moves on rather than burning tokens indefinitely.
|
|
82
|
+
|
|
83
|
+
**Pattern:**
|
|
84
|
+
```ts
|
|
85
|
+
let found = false;
|
|
86
|
+
for (let seed = 0; seed < MAX_SEEDS && !found; seed++) {
|
|
87
|
+
// ...
|
|
88
|
+
}
|
|
89
|
+
if (!found) {
|
|
90
|
+
console.error(`HALT: exhausted ${MAX_SEEDS} seeds without finding a valid fixture`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## §5 Render/deploy waits are bounded and always followed by a smoke test
|
|
98
|
+
|
|
99
|
+
**Summary:** Render (and similar) deploys may take 2–10 minutes and may silently fail. Always bound the wait and follow it with a live endpoint test.
|
|
100
|
+
|
|
101
|
+
**Pattern:**
|
|
102
|
+
```bash
|
|
103
|
+
DEPLOY_OK=0
|
|
104
|
+
for i in $(seq 1 20); do
|
|
105
|
+
if curl -sf https://bilko.run/projects/<slug>/ > /dev/null; then
|
|
106
|
+
DEPLOY_OK=1; echo "deploy live (attempt $i)"; break
|
|
107
|
+
fi
|
|
108
|
+
echo "waiting for deploy ($i/20)..."
|
|
109
|
+
sleep 15
|
|
110
|
+
done
|
|
111
|
+
# Continue regardless. Smoke test below catches the real failure.
|
|
112
|
+
if [ $DEPLOY_OK -eq 0 ]; then
|
|
113
|
+
echo "WARNING: deploy not detected after 20 attempts; continuing to smoke test"
|
|
114
|
+
fi
|
|
115
|
+
curl -sf https://bilko.run/projects/<slug>/ > /dev/null || { echo "SMOKE TEST FAILED"; exit 1; }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Rule:** A static-content deploy on Render does NOT restart the backend API. Never use `uptime` or process-restart signals to detect a static-content deploy — use the actual URL that should be live.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## §6 Frontmatter rules
|
|
123
|
+
|
|
124
|
+
**Summary:** Required keys are `title`, `cwd` (absolute path), `estimateMinutes`. Default to letting the filename `NN-` prefix drive grouping; include `parallelGroup` ONLY when you are deliberately interleaving across project streams.
|
|
125
|
+
|
|
126
|
+
**Required frontmatter:**
|
|
127
|
+
```yaml
|
|
128
|
+
---
|
|
129
|
+
title: <one line, plain English>
|
|
130
|
+
cwd: ~/Projects/<target-repo>
|
|
131
|
+
estimateMinutes: 60
|
|
132
|
+
---
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Cross-machine portability:** Write `cwd` as `~/Projects/<name>` — the parser expands `~` to `os.homedir()` at ingest time, so the same PRD file works on Linux (`/home/<u>/...`) and macOS (`/Users/<u>/...`). Absolute paths (e.g. `/home/bilko/Projects/foo`) are passed through unchanged and will break on any machine with a different home directory.
|
|
136
|
+
|
|
137
|
+
**Rules:**
|
|
138
|
+
- `cwd` MUST point to the target project. Prefer `~/...` for portability; only use an absolute path if you have a specific reason to pin to one machine.
|
|
139
|
+
- **`cwd` MUST already exist on disk at queue time.** The scheduler runs a dead-cwd guard (`fs.accessSync(cwd, fs.constants.X_OK)` in `src/main/scheduler.cjs:669-680`) *before* spawning the child, so a PRD whose `cwd` references a not-yet-created directory will exit with `-1: cwd no longer exists` and the body will never run — even if the first step of the body would have created the directory. If the PRD's purpose is to create a brand-new sibling project at `~/Projects/<new-slug>/`, point `cwd` at the parent (`~/Projects`) and make the first executable step `mkdir -p ~/Projects/<new-slug> && cd ~/Projects/<new-slug>`.
|
|
140
|
+
- `estimateMinutes` is used for ETA display; include a realistic estimate (note: empirical median is ~10 min, p90 ~20 min — avoid wildly inflated estimates that hide real outliers).
|
|
141
|
+
- `parallelGroup` in frontmatter, when present, IS honored by the scheduler (`pickNextBatch` reads `parallelGroup ?? 99` and overrides the filename NN). Use this only for cross-stream interleaving — e.g., the cellar series `122-`, `123-`, `124-` overrides to groups `113`, `114`, `115` so cellar steps fire alongside the parallel etch steps. Do NOT use it to reorder within a single stream; rename the file instead.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## §7 Self-containment
|
|
146
|
+
|
|
147
|
+
**Summary:** The PRD body is the executor's entire context. It runs as `claude -p "<body>"` with no conversation history.
|
|
148
|
+
|
|
149
|
+
**Rule:** Include exact file paths, function signatures if they save a Read, library versions, and the name of any sibling PRD the executor must NOT duplicate. Do not reference "the conversation", "what we discussed", "the design doc", or any other external context. If the executor would need to search for something, include the answer.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## §8 Scope sizing — target ~15 min, ceiling 30 (data-driven, 2026-06)
|
|
154
|
+
|
|
155
|
+
**Summary:** One PRD ≈ **~15 wall-clock minutes** of work. Empirically (400+ runs) median real run = **~7 min**, p90 = **21 min**; authored estimates ran 5–8× too high. **If you project >30 min, SPLIT.**
|
|
156
|
+
|
|
157
|
+
**Rule:** Split larger work into sequential PRDs; reference the dependency in `# Implementation notes`. PRDs in the same `NN-` group run in parallel — don't put dependent work in the same group. e2e/publish work is the failure tail: **shard test suites to one spec per PRD; never run a full suite or an endpoint-polling publish in a single PRD** (§1/§5).
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## §9 Failure surfacing
|
|
162
|
+
|
|
163
|
+
**Summary:** Prefer `exit 1` with a one-line diagnosis over silent retries. Note: a `rateLimited` exit-1 is the scheduler's benign auto-pause (it auto-resumes at the next 5h reset), NOT an authoring failure — don't engineer retry logic for it.
|
|
164
|
+
|
|
165
|
+
**Rule:** When a step fails, print a single diagnostic line and exit 1. The scheduler marks the job `failed` and the investigator Claude reads the log. A clean failure message is worth more than a 15-minute silent retry loop. Do not swallow errors with `|| true` unless the failure is genuinely non-fatal and you explain why.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# RIGHT
|
|
169
|
+
npm test || { echo "HALT: npm test failed — see above"; exit 1; }
|
|
170
|
+
|
|
171
|
+
# WRONG
|
|
172
|
+
npm test || true # silently continues even if tests are broken
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## §10 Pre-queue checklist (the litany)
|
|
178
|
+
|
|
179
|
+
Before queueing a new PRD, verify each of these:
|
|
180
|
+
|
|
181
|
+
- [ ] **§1 Bounded waits:** Every `until`/`while` poll has a `for i in $(seq 1 N)` cap ≤ 20 iterations.
|
|
182
|
+
- [ ] **Every command bounded:** Every test/build/dev-server/deploy command is wrapped in `timeout` (typecheck/unit 300s, e2e 120s, `curl --max-time 15`). No bare `playwright test` / `vite` / `pnpm dev` / `curl … | head`.
|
|
183
|
+
- [ ] **§2 No bonus work:** AC list is the only source of work. No "while we're here" additions.
|
|
184
|
+
- [ ] **§3 Smoke tests + verify-before-done:** Every deploy/migration step is followed by a test command that exits 1 on failure. Run the AC test command once before declaring done; never end the run on a red test.
|
|
185
|
+
- [ ] **§4 Bounded generators:** Any search/seed loop has an explicit `MAX_ATTEMPTS` constant and surfaces failure on exhaustion.
|
|
186
|
+
- [ ] **§5 Render deploys:** Deploy waits use a live URL check, not uptime/restart signals.
|
|
187
|
+
- [ ] **§6 Frontmatter:** `title`, `cwd` (`~/Projects/<name>` preferred; path MUST exist on this machine), `estimateMinutes` present. `parallelGroup` only if intentionally interleaving cross-stream.
|
|
188
|
+
- [ ] **§7 Self-contained:** No references to "the conversation" or external context. Paths and identifiers are inline. Body is clean UTF-8 — **no NUL/control bytes** (paste-from-PDF crashes the spawn). Quick check: `grep -qP '\x00' file && echo BAD`.
|
|
189
|
+
- [ ] **§8 Scope:** Targets ~15 min, ceiling 30. If projected larger, split. e2e/publish sharded to one spec per PRD.
|
|
190
|
+
- [ ] **§9 Failure surfacing:** Errors exit 1 with a diagnostic line. No silent `|| true` swallows. (`rateLimited` exit-1 is benign auto-pause, not a failure.)
|
|
191
|
+
- [ ] **§11 Negative-assertion checks:** Any "this should produce NO output / NO match" check (a `grep` that should find nothing, a "no leftover X" guard) is written as an inverted conditional that exits 0 on the clean case. A bare `grep` whose success is "no match" exits 1 and trips the verifier `transcript_errors` downgrade even when the run is perfect.
|
|
192
|
+
- [ ] **§12 End green:** The acceptance/test gate is the LAST thing the run does; any intentionally-failing step (TDD red test, expected-nonzero probe) runs EARLY, never after the gate, and is captured (`2>&1 | tail` inside a conditional) so it doesn't surface as a bare `is_error`/`Traceback` in the final portion of the transcript.
|
|
193
|
+
- [ ] **§13 Recover/annotate errors & expected timeouts:** A throwaway probe that errors is re-run corrected (or annotated `# expected/handled`) right after — never left stranded; prefer a temp `.py` over a fragile inline `python -c`. An *expected* `timeout` cap (a long ingest/scan) handles exit 124 explicitly as success-with-note, not a bare `Exit code 124`. Both prevent the `transcript_errors` downgrade of a green deliverable.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## §11 Negative-assertion checks must exit 0 on the clean case
|
|
198
|
+
|
|
199
|
+
**Summary:** A check that asserts the *absence* of something must return exit 0 when the
|
|
200
|
+
thing is absent. The classic trap is `grep`: it exits **1 when it finds no match**. If your
|
|
201
|
+
AC says "verify no banned phrase remains" and you write a bare `grep`, the *success* path
|
|
202
|
+
(nothing found) surfaces as `is_error=true` in the transcript — and the verifier's
|
|
203
|
+
`transcript_errors` heuristic downgrades the whole run to `needs_review` even though it did
|
|
204
|
+
everything right.
|
|
205
|
+
|
|
206
|
+
**This actually happened** (PRD `62-x-trader-doctrine`, 2026-06-13): the doctrine was cleaned
|
|
207
|
+
correctly and committed, but the AC's sanity grep —
|
|
208
|
+
`grep -rniE "building in public|..." data/pipelines/x_session/` — found nothing, exited 1,
|
|
209
|
+
and a perfect run was flagged for review. Self-inflicted, by the PRD author.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# WRONG — exits 1 (is_error) exactly when the check PASSES
|
|
213
|
+
grep -rniE "building in public|indie hacker" data/pipelines/x_session/
|
|
214
|
+
|
|
215
|
+
# RIGHT — inverted: "found banned phrase" is the failure, "clean" exits 0
|
|
216
|
+
if grep -rniE "building in public|indie hacker" data/pipelines/x_session/; then
|
|
217
|
+
echo "HALT: banned builder framing still present (see matches above)"; exit 1
|
|
218
|
+
fi
|
|
219
|
+
echo "clean: no banned framing"
|
|
220
|
+
|
|
221
|
+
# ALSO RIGHT — grep -q with negation, when you don't need to see the matches
|
|
222
|
+
grep -rqniE "building in public|indie hacker" data/pipelines/x_session/ \
|
|
223
|
+
&& { echo "HALT: banned framing present"; exit 1; } || echo "clean"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Rule:** Whenever an AC line is phrased as "verify there are no…", "confirm X does not
|
|
227
|
+
appear", "no leftover…", write it as `if <detector>; then echo HALT…; exit 1; fi`. Never let
|
|
228
|
+
the no-match/empty-output path be the one that carries a non-zero exit. Applies to `grep`,
|
|
229
|
+
`rg`, `find ... | grep`, `diff` (exits 1 on differences), and any custom detector.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## §12 End green, and trust the verdict sentinel
|
|
234
|
+
|
|
235
|
+
**Summary:** The post-run verifier (`runVerify.cjs`) scans the transcript and downgrades to
|
|
236
|
+
`needs_review` on error markers (`Traceback`+`Error`, `FAIL`/`FATAL`, a tool `is_error` in the
|
|
237
|
+
final portion of the run). It cannot tell an *intentional* failure from a real one. Two rules
|
|
238
|
+
keep legitimate runs from false-tripping it.
|
|
239
|
+
|
|
240
|
+
**12a — Run the green gate LAST.** Order the run so the final command is the acceptance/test
|
|
241
|
+
gate. Do any intentionally-failing step EARLY:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# WRONG — red test reproduced AFTER the work; its Traceback lands late in the transcript
|
|
245
|
+
pytest -q # all green
|
|
246
|
+
python -m pytest tests/test_repro.py::test_bug # ← TDD red demo, errors, trips verifier
|
|
247
|
+
|
|
248
|
+
# RIGHT — red demo first (and captured), green gate last
|
|
249
|
+
python -m pytest tests/test_repro.py::test_bug 2>&1 | tail -3 || true # expected red, captured
|
|
250
|
+
# ... implement the fix ...
|
|
251
|
+
timeout 300 pytest -q # ← LAST thing the run does; ends green
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
If you must show a failure late, capture it (`… 2>&1 | tail` inside a conditional, or assert
|
|
255
|
+
on the captured text) so a raw `Traceback`/`is_error` never hits the transcript bare.
|
|
256
|
+
|
|
257
|
+
**12b — The `SCHEDULER_VERDICT` sentinel is authoritative; emit it truthfully.** The scheduler's
|
|
258
|
+
FINISH PROTOCOL ends by printing `SCHEDULER_VERDICT: PASS` once the AC gate is green AND the
|
|
259
|
+
commit landed (else `SCHEDULER_VERDICT: FAIL <reason>` + `exit 1`). The verifier treats
|
|
260
|
+
`PASS` + a commit landed during the run as the **authoritative** signal and overrides incidental
|
|
261
|
+
transcript markers — this is what lets a deliberately-reproduced red test (PRD 77) or a grep
|
|
262
|
+
result containing "Error" (PRD 68) finish `completed` instead of `needs_review`. **Never print
|
|
263
|
+
`PASS` on a red gate.** The sentinel is only a safety net while it tells the truth; a lying
|
|
264
|
+
`PASS` converts the verifier from "catches false failures" into "ships silent failures."
|
|
265
|
+
|
|
266
|
+
**This actually happened** (PRDs 68 + 77, 2026-06-13): both committed correct work with green
|
|
267
|
+
suites, but 77's `systematic-debugging` red-test repro and 68's grep-"Error" substring each
|
|
268
|
+
tripped `transcript_errors → needs_review`, and the self-heal pass kept re-deriving the same
|
|
269
|
+
verdict from the immutable log — stuck indefinitely. §12 (end-green + authoritative sentinel)
|
|
270
|
+
is the structural fix.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## §13 Don't strand mid-run probe errors; annotate expected timeouts
|
|
275
|
+
|
|
276
|
+
**Summary:** §12 keeps the *final* portion of the transcript green. §13 covers the *middle* —
|
|
277
|
+
two executor habits that strand a bare `Traceback`/`Error`/`Exit code` the verifier then flags,
|
|
278
|
+
even when the deliverable is correct and committed.
|
|
279
|
+
|
|
280
|
+
**13a — A throwaway probe that errors must recover or be annotated in place.** Exploratory
|
|
281
|
+
`python -c`/`bash` probes that error (a quoting/f-string slip, a wrong kwarg, a bad path) leave a
|
|
282
|
+
bare traceback. Re-run the corrected probe immediately, or print `# expected/handled: <why>` on
|
|
283
|
+
the next line, so recovery is adjacent (the heuristic looks for recovery within ~10 lines).
|
|
284
|
+
Prefer a small temp `.py` file over a fragile multi-quote `python -c` one-liner — inline
|
|
285
|
+
f-string/quoting errors are the top source of stranded probe tracebacks.
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# WRONG — inline f-string slip strands a SyntaxError, then you move on
|
|
289
|
+
python -c 'print(f"{p["title"]!r[:40]}")' # SyntaxError, bare in transcript
|
|
290
|
+
|
|
291
|
+
# RIGHT — write the probe to a temp file (no shell-quote minefield), or annotate
|
|
292
|
+
cat > /tmp/probe.py <<'PY'
|
|
293
|
+
print(repr(p["title"])[:40])
|
|
294
|
+
PY
|
|
295
|
+
python /tmp/probe.py || echo "# expected/handled: probe only, not part of the deliverable"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**13b — An *expected* `timeout` cap is success-with-note, not a bare `Exit code 124`.** Capping a
|
|
299
|
+
genuinely long task you expect to hit the cap (a full-universe ingest, a long scan) is the
|
|
300
|
+
correct §1/§8 behavior — but a bare `Exit code 124` reads as failure to the verifier. Branch on
|
|
301
|
+
124 explicitly:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
timeout 120 python -m project.ingest --all || { rc=$?
|
|
305
|
+
[ $rc -eq 124 ] && echo "hit time cap — idempotent/partial; rows persist incrementally; OK" \
|
|
306
|
+
|| { echo "HALT: ingest failed rc=$rc"; exit 1; }; }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
For work that legitimately needs longer than a safe cap, run it in the background and poll a
|
|
310
|
+
bounded number of times (§1) rather than capping the foreground command.
|
|
311
|
+
|
|
312
|
+
**This actually happened** (PRDs 77 + 80, 2026-06-13): 77 stranded an inline-`python -c` f-string
|
|
313
|
+
`SyntaxError` from a throwaway permalink probe; 80 surfaced a bare `Exit code 124` from a
|
|
314
|
+
`timeout`-capped full-universe EDGAR ingest. Both committed correct, green, AC-complete work
|
|
315
|
+
(77's cursor-hold fix; 80's 6 EDGAR rows + installed cron) yet were downgraded to `needs_review`
|
|
316
|
+
on the incidental middle-of-run markers. 13a/13b keep the middle of the transcript clean.
|
package/src/main/usage.cjs
CHANGED
|
@@ -24,6 +24,39 @@ const { refreshIfNeeded, expiresAtMs } = require('./lib/credentials.cjs');
|
|
|
24
24
|
const { writeJson } = require('./config.cjs');
|
|
25
25
|
|
|
26
26
|
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
27
|
+
|
|
28
|
+
/** A non-empty env value that isn't an explicit falsey string. */
|
|
29
|
+
function envEnabled(v) {
|
|
30
|
+
return v != null && v !== '' && v !== '0' && String(v).toLowerCase() !== 'false';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Is the consumer 5-hour usage meter (/api/oauth/usage) even applicable here?
|
|
35
|
+
*
|
|
36
|
+
* That endpoint only exists for OAuth/subscription auth against
|
|
37
|
+
* api.anthropic.com. Enterprise auth modes have no such meter, so polling it
|
|
38
|
+
* just 404s/times-out — and the scheduler must NOT gate on (or pause for) it.
|
|
39
|
+
* Detected modes: Amazon Bedrock, Google Vertex, raw API-key, a custom auth
|
|
40
|
+
* token, or a non-Anthropic base URL (corporate gateway/proxy).
|
|
41
|
+
*
|
|
42
|
+
* Returns false → caller should treat usage as unavailable-by-design and fire
|
|
43
|
+
* work on its own (pending + memory) instead of waiting on a meter.
|
|
44
|
+
*/
|
|
45
|
+
function usageMeterApplicable(env = process.env) {
|
|
46
|
+
if (envEnabled(env.CLAUDE_CODE_USE_BEDROCK)) return false;
|
|
47
|
+
if (envEnabled(env.CLAUDE_CODE_USE_VERTEX)) return false;
|
|
48
|
+
if (env.ANTHROPIC_API_KEY) return false;
|
|
49
|
+
if (env.ANTHROPIC_AUTH_TOKEN) return false;
|
|
50
|
+
if (env.ANTHROPIC_BASE_URL) {
|
|
51
|
+
// Parse the host rather than substring-match, so a deceptive gateway like
|
|
52
|
+
// https://anthropic.com.attacker.example is correctly treated as enterprise.
|
|
53
|
+
let host;
|
|
54
|
+
try { host = new URL(env.ANTHROPIC_BASE_URL).hostname.toLowerCase(); }
|
|
55
|
+
catch { return false; } // unparseable custom URL → treat as a gateway
|
|
56
|
+
if (host !== 'anthropic.com' && !host.endsWith('.anthropic.com')) return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
27
60
|
const CACHE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'billing-cache.json');
|
|
28
61
|
// Coalesce the 4 renderer pollers (Overview/AppStatusBar/StatusBar/Usage). A
|
|
29
62
|
// fresh ok-cache is served directly without touching the network. Auth/
|
|
@@ -164,4 +197,4 @@ function registerBillingHandlers() {
|
|
|
164
197
|
});
|
|
165
198
|
}
|
|
166
199
|
|
|
167
|
-
module.exports = { registerBillingHandlers, fetchUsage, classifyUsageResponse };
|
|
200
|
+
module.exports = { registerBillingHandlers, fetchUsage, classifyUsageResponse, usageMeterApplicable };
|
package/src/preload/api.d.ts
CHANGED
|
@@ -1025,8 +1025,14 @@ export interface SessionManagerAPI {
|
|
|
1025
1025
|
};
|
|
1026
1026
|
plugins: {
|
|
1027
1027
|
/** Run `claude plugin install <slug>` in a hidden pty. Streams output
|
|
1028
|
-
* via `onInstallProgress`. Returns { ok, exitCode } on exit.
|
|
1029
|
-
|
|
1028
|
+
* via `onInstallProgress`. Returns { ok, exitCode } on exit.
|
|
1029
|
+
* Pass `marketplace` for a non-official plugin: the source is registered
|
|
1030
|
+
* via `claude plugin marketplace add <add>` first, then `<slug>@<name>`
|
|
1031
|
+
* is installed. */
|
|
1032
|
+
install: (payload: {
|
|
1033
|
+
slug: string;
|
|
1034
|
+
marketplace?: { add: string; name: string };
|
|
1035
|
+
}) => Promise<PluginInstallResult>;
|
|
1030
1036
|
onInstallProgress: (handler: (ev: PluginInstallProgressEvent) => void) => () => void;
|
|
1031
1037
|
};
|
|
1032
1038
|
clipboard: {
|