devharness 0.9.3 → 0.9.4
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/build/block-events.d.ts +42 -0
- package/build/block-events.d.ts.map +1 -0
- package/build/block-events.js +67 -0
- package/build/block-events.js.map +1 -0
- package/build/block-events.test.d.ts +2 -0
- package/build/block-events.test.d.ts.map +1 -0
- package/build/block-events.test.js +131 -0
- package/build/block-events.test.js.map +1 -0
- package/build/index.js +33 -2
- package/build/index.js.map +1 -1
- package/build/server-block-cleanup.test.d.ts +2 -0
- package/build/server-block-cleanup.test.d.ts.map +1 -0
- package/build/server-block-cleanup.test.js +143 -0
- package/build/server-block-cleanup.test.js.map +1 -0
- package/build/server-manager.d.ts +8 -0
- package/build/server-manager.d.ts.map +1 -1
- package/build/server-manager.js +49 -0
- package/build/server-manager.js.map +1 -1
- package/build/startup-gate.d.ts +29 -0
- package/build/startup-gate.d.ts.map +1 -0
- package/build/startup-gate.js +49 -0
- package/build/startup-gate.js.map +1 -0
- package/build/startup-gate.test.d.ts +2 -0
- package/build/startup-gate.test.d.ts.map +1 -0
- package/build/startup-gate.test.js +73 -0
- package/build/startup-gate.test.js.map +1 -0
- package/build/tool-response.d.ts +3 -0
- package/build/tool-response.d.ts.map +1 -1
- package/build/tool-response.js +46 -1
- package/build/tool-response.js.map +1 -1
- package/build/tools/assert-tools.d.ts +3 -3
- package/build/tools/breakpoint-tools.d.ts +3 -3
- package/build/tools/config-tools.d.ts +3 -3
- package/build/tools/console-tools.d.ts +9 -9
- package/build/tools/content-tools.d.ts +3 -3
- package/build/tools/input-tools.d.ts +9 -9
- package/build/tools/issues-tools.d.ts +6 -6
- package/build/tools/page-tools.d.ts +3 -3
- package/build/tools/replay-tools.d.ts +9 -9
- package/build/tools/request-tools.d.ts +3 -3
- package/build/tools/server-tools.d.ts +9 -9
- package/build/tools/storage-tools.d.ts +3 -3
- package/build/tools/wait-tools.d.ts +6 -6
- package/docs/instructions.md +23 -0
- package/package.json +1 -1
- package/plugin/skills/devharness/SKILL.md +18 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block event stream - one JSON line per new guard block, appended to
|
|
3
|
+
* .devharness/logs/blocks.jsonl.
|
|
4
|
+
*
|
|
5
|
+
* Guards only surface on the *next* tool call, so a session doing unrelated
|
|
6
|
+
* work never learns its dev server died until it happens to call a devharness
|
|
7
|
+
* tool again. This file is the push side of that: a Claude Code Monitor can
|
|
8
|
+
* `tail -f` it and get notified the moment a block appears.
|
|
9
|
+
*
|
|
10
|
+
* Deduplicated by key - a block that keeps firing on every subsequent tool call
|
|
11
|
+
* writes one line, not one per call. The active set clears once a call gets
|
|
12
|
+
* through all guards.
|
|
13
|
+
*/
|
|
14
|
+
export type BlockGuard = 'port' | 'breakpoint' | 'pendingStartup' | 'bug' | 'duplicateSession';
|
|
15
|
+
/**
|
|
16
|
+
* Identity and summary of a block, attached to the BlockingResponse that
|
|
17
|
+
* carries it so index.ts can log it without re-deriving the reason.
|
|
18
|
+
*/
|
|
19
|
+
export interface BlockEventInfo {
|
|
20
|
+
guard: BlockGuard;
|
|
21
|
+
/** Dedupe identity - same key while the same block persists. */
|
|
22
|
+
key: string;
|
|
23
|
+
/** One-line summary of what is blocked. */
|
|
24
|
+
detail: string;
|
|
25
|
+
/** The call that clears it. */
|
|
26
|
+
resolve: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Append a block event if this block is new. No-op for a block already logged.
|
|
30
|
+
*/
|
|
31
|
+
export declare function recordBlockEvent(info: BlockEventInfo | undefined, tool: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Forget every active block. Called when a tool call clears all guards, so a
|
|
34
|
+
* block that recurs later gets logged again.
|
|
35
|
+
*/
|
|
36
|
+
export declare function clearBlockEvents(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Test/debug helper - which blocks are currently deduped out.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getActiveBlockKeys(): string[];
|
|
41
|
+
export declare function getBlockEventFilePath(): string;
|
|
42
|
+
//# sourceMappingURL=block-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-events.d.ts","sourceRoot":"","sources":["../src/block-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,YAAY,GACZ,gBAAgB,GAChB,KAAK,GACL,kBAAkB,CAAC;AAEvB;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,UAAU,CAAC;IAClB,gEAAgE;IAChE,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;CACjB;AAUD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,cAAc,GAAG,SAAS,EAChC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block event stream - one JSON line per new guard block, appended to
|
|
3
|
+
* .devharness/logs/blocks.jsonl.
|
|
4
|
+
*
|
|
5
|
+
* Guards only surface on the *next* tool call, so a session doing unrelated
|
|
6
|
+
* work never learns its dev server died until it happens to call a devharness
|
|
7
|
+
* tool again. This file is the push side of that: a Claude Code Monitor can
|
|
8
|
+
* `tail -f` it and get notified the moment a block appears.
|
|
9
|
+
*
|
|
10
|
+
* Deduplicated by key - a block that keeps firing on every subsequent tool call
|
|
11
|
+
* writes one line, not one per call. The active set clears once a call gets
|
|
12
|
+
* through all guards.
|
|
13
|
+
*/
|
|
14
|
+
import { promises as fs } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { getOutputPath } from './helpers/paths.js';
|
|
17
|
+
// Keys currently blocking. Written once per key, cleared when a tool call
|
|
18
|
+
// passes every guard.
|
|
19
|
+
const activeKeys = new Set();
|
|
20
|
+
function getBlockFile() {
|
|
21
|
+
return join(getOutputPath('logs'), 'blocks.jsonl');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Append a block event if this block is new. No-op for a block already logged.
|
|
25
|
+
*/
|
|
26
|
+
export async function recordBlockEvent(info, tool) {
|
|
27
|
+
if (!info)
|
|
28
|
+
return;
|
|
29
|
+
const dedupeKey = `${info.guard}:${info.key}`;
|
|
30
|
+
if (activeKeys.has(dedupeKey)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
activeKeys.add(dedupeKey);
|
|
34
|
+
const line = JSON.stringify({
|
|
35
|
+
ts: new Date().toISOString(),
|
|
36
|
+
guard: info.guard,
|
|
37
|
+
tool,
|
|
38
|
+
detail: info.detail,
|
|
39
|
+
resolve: info.resolve,
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
const dir = getOutputPath('logs');
|
|
43
|
+
await fs.mkdir(dir, { recursive: true });
|
|
44
|
+
await fs.appendFile(getBlockFile(), line + '\n');
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// Never let the event stream break the block itself
|
|
48
|
+
console.error(`[BlockEvents] Failed to write block event: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Forget every active block. Called when a tool call clears all guards, so a
|
|
53
|
+
* block that recurs later gets logged again.
|
|
54
|
+
*/
|
|
55
|
+
export function clearBlockEvents() {
|
|
56
|
+
activeKeys.clear();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Test/debug helper - which blocks are currently deduped out.
|
|
60
|
+
*/
|
|
61
|
+
export function getActiveBlockKeys() {
|
|
62
|
+
return [...activeKeys];
|
|
63
|
+
}
|
|
64
|
+
export function getBlockEventFilePath() {
|
|
65
|
+
return getBlockFile();
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=block-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-events.js","sourceRoot":"","sources":["../src/block-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAuBnD,0EAA0E;AAC1E,sBAAsB;AACtB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;AAErC,SAAS,YAAY;IACnB,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC,CAAC;AACrD,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAgC,EAChC,IAAY;IAEZ,IAAI,CAAC,IAAI;QAAE,OAAO;IAElB,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IAC9C,IAAI,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,OAAO;IACT,CAAC;IACD,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAE1B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;QAC1B,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAC5B,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI;QACJ,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QAClC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,MAAM,EAAE,CAAC,UAAU,CAAC,YAAY,EAAE,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,oDAAoD;QACpD,OAAO,CAAC,KAAK,CAAC,8CAA8C,KAAK,EAAE,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,UAAU,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-events.test.d.ts","sourceRoot":"","sources":["../src/block-events.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blocks.jsonl is what a Claude Code Monitor tails, so a notification storm is
|
|
3
|
+
* the failure mode that matters: guards re-fire on every subsequent tool call,
|
|
4
|
+
* and a naive append would emit one line per blocked call instead of one per
|
|
5
|
+
* block. These cover the dedupe, the clear-on-unblocked reset, and that each
|
|
6
|
+
* guard actually attaches the info the line is built from.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { initializePaths } from './helpers/paths.js';
|
|
13
|
+
import { recordBlockEvent, clearBlockEvents, getBlockEventFilePath, } from './block-events.js';
|
|
14
|
+
import { checkPortFailures, checkPendingStartups, checkDuplicateSession, } from './tool-response.js';
|
|
15
|
+
let workDir;
|
|
16
|
+
let originalCwd;
|
|
17
|
+
let originalGlobalDir;
|
|
18
|
+
const portBlock = {
|
|
19
|
+
guard: 'port',
|
|
20
|
+
key: '3000',
|
|
21
|
+
detail: 'Monitored port(s) down: 3000 (web)',
|
|
22
|
+
resolve: `server({ action: 'acknowledgePort', port: 3000 })`,
|
|
23
|
+
};
|
|
24
|
+
function lines() {
|
|
25
|
+
const path = getBlockEventFilePath();
|
|
26
|
+
if (!existsSync(path))
|
|
27
|
+
return [];
|
|
28
|
+
return readFileSync(path, 'utf-8')
|
|
29
|
+
.split('\n')
|
|
30
|
+
.filter(l => l.trim())
|
|
31
|
+
.map(l => JSON.parse(l));
|
|
32
|
+
}
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
originalCwd = process.cwd();
|
|
35
|
+
workDir = mkdtempSync(join(tmpdir(), 'cdp-block-events-test-'));
|
|
36
|
+
process.chdir(workDir);
|
|
37
|
+
originalGlobalDir = process.env.CDP_TOOLS_DIR;
|
|
38
|
+
process.env.CDP_TOOLS_DIR = join(workDir, 'global');
|
|
39
|
+
initializePaths();
|
|
40
|
+
clearBlockEvents();
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
clearBlockEvents();
|
|
44
|
+
process.chdir(originalCwd);
|
|
45
|
+
if (originalGlobalDir === undefined)
|
|
46
|
+
delete process.env.CDP_TOOLS_DIR;
|
|
47
|
+
else
|
|
48
|
+
process.env.CDP_TOOLS_DIR = originalGlobalDir;
|
|
49
|
+
initializePaths();
|
|
50
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
describe('block event stream', () => {
|
|
53
|
+
it('writes one line per block, not per blocked call', async () => {
|
|
54
|
+
await recordBlockEvent(portBlock, 'navigate');
|
|
55
|
+
await recordBlockEvent(portBlock, 'screenshot');
|
|
56
|
+
await recordBlockEvent(portBlock, 'content');
|
|
57
|
+
expect(lines()).toHaveLength(1);
|
|
58
|
+
expect(lines()[0]).toMatchObject({
|
|
59
|
+
guard: 'port',
|
|
60
|
+
tool: 'navigate',
|
|
61
|
+
detail: 'Monitored port(s) down: 3000 (web)',
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it('logs the same block again after a call clears every guard', async () => {
|
|
65
|
+
await recordBlockEvent(portBlock, 'navigate');
|
|
66
|
+
clearBlockEvents();
|
|
67
|
+
await recordBlockEvent(portBlock, 'navigate');
|
|
68
|
+
expect(lines()).toHaveLength(2);
|
|
69
|
+
});
|
|
70
|
+
it('treats a different block as a new event while the first is still active', async () => {
|
|
71
|
+
await recordBlockEvent(portBlock, 'navigate');
|
|
72
|
+
await recordBlockEvent({ ...portBlock, key: '3000,5173', detail: 'Monitored port(s) down: 3000, 5173' }, 'navigate');
|
|
73
|
+
expect(lines()).toHaveLength(2);
|
|
74
|
+
});
|
|
75
|
+
it('separates guards that happen to share a key', async () => {
|
|
76
|
+
await recordBlockEvent({ ...portBlock, guard: 'port', key: 'web' }, 'navigate');
|
|
77
|
+
await recordBlockEvent({ ...portBlock, guard: 'pendingStartup', key: 'web' }, 'navigate');
|
|
78
|
+
expect(lines().map(l => l.guard)).toEqual(['port', 'pendingStartup']);
|
|
79
|
+
});
|
|
80
|
+
it('does nothing when a guard supplies no block info', async () => {
|
|
81
|
+
await recordBlockEvent(undefined, 'navigate');
|
|
82
|
+
expect(lines()).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('guards attach block info', () => {
|
|
86
|
+
it('port failure', () => {
|
|
87
|
+
const failed = [
|
|
88
|
+
{ port: 3000, level: 'block', failedAt: new Date('2026-01-01T00:00:00Z'), description: 'web' },
|
|
89
|
+
];
|
|
90
|
+
const result = checkPortFailures(failed, 'navigate');
|
|
91
|
+
expect(result.blocked).toBe(true);
|
|
92
|
+
if (!result.blocked)
|
|
93
|
+
return;
|
|
94
|
+
expect(result.block?.guard).toBe('port');
|
|
95
|
+
expect(result.block?.key).toBe('3000');
|
|
96
|
+
expect(result.block?.resolve).toContain('acknowledgePort');
|
|
97
|
+
});
|
|
98
|
+
it('pending startup failure', () => {
|
|
99
|
+
const failures = [
|
|
100
|
+
{ serverId: 'web', reason: 'died', startedAt: new Date('2026-01-01T00:00:00Z') },
|
|
101
|
+
];
|
|
102
|
+
const result = checkPendingStartups(failures, 'navigate');
|
|
103
|
+
expect(result.blocked).toBe(true);
|
|
104
|
+
if (!result.blocked)
|
|
105
|
+
return;
|
|
106
|
+
expect(result.block?.guard).toBe('pendingStartup');
|
|
107
|
+
expect(result.block?.detail).toContain('died before port detected');
|
|
108
|
+
expect(result.block?.resolve).toContain('acknowledgeStartup');
|
|
109
|
+
});
|
|
110
|
+
it('duplicate session, from both sides', () => {
|
|
111
|
+
const base = {
|
|
112
|
+
sessionId: 'abc-123',
|
|
113
|
+
shortId: 'abc',
|
|
114
|
+
allPids: [10, 20],
|
|
115
|
+
allPpids: [100, 200],
|
|
116
|
+
};
|
|
117
|
+
const original = checkDuplicateSession({ ...base, currentPid: 10, currentPpid: 100 }, 'navigate');
|
|
118
|
+
expect(original.blocked).toBe(true);
|
|
119
|
+
if (original.blocked) {
|
|
120
|
+
expect(original.block?.key).toBe('original:abc-123');
|
|
121
|
+
expect(original.block?.resolve).toBe('kill 200');
|
|
122
|
+
}
|
|
123
|
+
const duplicate = checkDuplicateSession({ ...base, currentPid: 20, currentPpid: 200 }, 'navigate');
|
|
124
|
+
expect(duplicate.blocked).toBe(true);
|
|
125
|
+
if (duplicate.blocked) {
|
|
126
|
+
expect(duplicate.block?.key).toBe('duplicate:abc-123');
|
|
127
|
+
expect(duplicate.block?.resolve).toContain('--fork-session');
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
//# sourceMappingURL=block-events.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-events.test.js","sourceRoot":"","sources":["../src/block-events.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GAEtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAG5B,IAAI,OAAe,CAAC;AACpB,IAAI,WAAmB,CAAC;AACxB,IAAI,iBAAqC,CAAC;AAE1C,MAAM,SAAS,GAAmB;IAChC,KAAK,EAAE,MAAM;IACb,GAAG,EAAE,MAAM;IACX,MAAM,EAAE,oCAAoC;IAC5C,OAAO,EAAE,mDAAmD;CAC7D,CAAC;AAEF,SAAS,KAAK;IACZ,MAAM,IAAI,GAAG,qBAAqB,EAAE,CAAC;IACrC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC;SAC/B,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACrB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,UAAU,CAAC,GAAG,EAAE;IACd,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC5B,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,wBAAwB,CAAC,CAAC,CAAC;IAChE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvB,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACpD,eAAe,EAAE,CAAC;IAClB,gBAAgB,EAAE,CAAC;AACrB,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACb,gBAAgB,EAAE,CAAC;IACnB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC3B,IAAI,iBAAiB,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;;QACjE,OAAO,CAAC,GAAG,CAAC,aAAa,GAAG,iBAAiB,CAAC;IACnD,eAAe,EAAE,CAAC;IAClB,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,gBAAgB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAC9C,MAAM,gBAAgB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QAChD,MAAM,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAE7C,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;YAC/B,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,oCAAoC;SAC7C,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,gBAAgB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAC9C,gBAAgB,EAAE,CAAC;QACnB,MAAM,gBAAgB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAE9C,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,KAAK,IAAI,EAAE;QACvF,MAAM,gBAAgB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAC9C,MAAM,gBAAgB,CACpB,EAAE,GAAG,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,oCAAoC,EAAE,EAChF,UAAU,CACX,CAAC;QAEF,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,gBAAgB,CAAC,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,UAAU,CAAC,CAAC;QAChF,MAAM,gBAAgB,CAAC,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,UAAU,CAAC,CAAC;QAE1F,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,gBAAgB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,cAAc,EAAE,GAAG,EAAE;QACtB,MAAM,MAAM,GAAsB;YAChC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC,EAAE,WAAW,EAAE,KAAK,EAAqB;SAClH,CAAC;QACF,MAAM,MAAM,GAAG,iBAAiB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACrD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO;QAC5B,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,QAAQ,GAAgC;YAC5C,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC,EAA+B;SAC9G,CAAC;QACF,MAAM,MAAM,GAAG,oBAAoB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC1D,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO;QAC5B,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACnD,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;QACpE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,IAAI,GAAG;YACX,SAAS,EAAE,SAAS;YACpB,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC;YACjB,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC;SACrB,CAAC;QAEF,MAAM,QAAQ,GAAG,qBAAqB,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,EAAE,UAAU,CAAC,CAAC;QAClG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrB,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;YACrD,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACnD,CAAC;QAED,MAAM,SAAS,GAAG,qBAAqB,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,EAAE,UAAU,CAAC,CAAC;QACnG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;YACtB,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;YACvD,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/build/index.js
CHANGED
|
@@ -55,6 +55,8 @@ import { ServerManager, detectAutoRestartCommand } from './server-manager.js';
|
|
|
55
55
|
import { configManager } from './config.js';
|
|
56
56
|
import { ToolError } from './tool-error.js';
|
|
57
57
|
import { checkPortFailures, checkBreakpointPause, checkBugBlocking, checkPendingStartups, checkDuplicateSession, prependToResponse, appendToResponse, buildStatusSuffix } from './tool-response.js';
|
|
58
|
+
import { recordBlockEvent, clearBlockEvents } from './block-events.js';
|
|
59
|
+
import { createStartupGate } from './startup-gate.js';
|
|
58
60
|
import { createSuccessResponse, createErrorResponse } from './messages.js';
|
|
59
61
|
import { setChromeLauncher } from './error-helpers.js';
|
|
60
62
|
import { createServer } from 'net';
|
|
@@ -83,6 +85,11 @@ const SERVER_VERSION = (() => {
|
|
|
83
85
|
return '0.0.0';
|
|
84
86
|
}
|
|
85
87
|
})();
|
|
88
|
+
/** Tool calls wait on this until serverManager.initialize() has restored state. */
|
|
89
|
+
const startupGate = createStartupGate({
|
|
90
|
+
timeoutMs: 30_000,
|
|
91
|
+
onTimeout: (ms) => console.error(`[devharness] Startup recovery still running after ${ms}ms - serving tools anyway`),
|
|
92
|
+
});
|
|
86
93
|
/**
|
|
87
94
|
* Which build is actually answering, so a session can tell whether the code it
|
|
88
95
|
* is calling is the code it just compiled.
|
|
@@ -1356,6 +1363,15 @@ function registerToolHandlers(server) {
|
|
|
1356
1363
|
isError: true
|
|
1357
1364
|
};
|
|
1358
1365
|
}
|
|
1366
|
+
// The transport starts serving before serverManager.initialize() has
|
|
1367
|
+
// restored state, so a call landing in that window sees an empty world: a
|
|
1368
|
+
// dead server's guard silently passes, and `acknowledgeStartup` in there
|
|
1369
|
+
// acknowledges nothing and reverts as soon as recovery lands. Wait for it.
|
|
1370
|
+
// `config` is exempt: it reads no server state, and `config restart` is the
|
|
1371
|
+
// escape hatch you want available precisely when recovery is what's stuck.
|
|
1372
|
+
if (toolName !== 'config') {
|
|
1373
|
+
await startupGate.wait();
|
|
1374
|
+
}
|
|
1359
1375
|
// Check for tool dependency conflicts (blocks ALL tools except config)
|
|
1360
1376
|
if (toolName !== 'config' && configManager.hasDependencyConflicts()) {
|
|
1361
1377
|
const conflicts = configManager.getDependencyConflicts();
|
|
@@ -1399,31 +1415,38 @@ Edit ${configPath} to resolve, then restart the MCP server.`,
|
|
|
1399
1415
|
const failedPorts = portMonitor.getFailedPorts();
|
|
1400
1416
|
const portCheck = checkPortFailures(failedPorts, toolName);
|
|
1401
1417
|
if (portCheck.blocked) {
|
|
1418
|
+
await recordBlockEvent(portCheck.block, toolName);
|
|
1402
1419
|
return portCheck.response;
|
|
1403
1420
|
}
|
|
1404
1421
|
// Check for breakpoint pauses (block tools until acknowledged or resumed)
|
|
1405
1422
|
const allConnections = connectionManager.getAllConnections();
|
|
1406
1423
|
const breakpointCheck = checkBreakpointPause(allConnections, toolName, (port) => serverManager.getPendingRestartByInspectorPort(port), validation.data?.action);
|
|
1407
1424
|
if (breakpointCheck.blocked) {
|
|
1425
|
+
await recordBlockEvent(breakpointCheck.block, toolName);
|
|
1408
1426
|
return breakpointCheck.response;
|
|
1409
1427
|
}
|
|
1410
1428
|
// Check for pending startup failures (block tools until acknowledged)
|
|
1411
1429
|
const pendingStartupFailures = serverManager.getPendingStartupFailures();
|
|
1412
1430
|
const pendingStartupCheck = checkPendingStartups(pendingStartupFailures, toolName);
|
|
1413
1431
|
if (pendingStartupCheck.blocked) {
|
|
1432
|
+
await recordBlockEvent(pendingStartupCheck.block, toolName);
|
|
1414
1433
|
return pendingStartupCheck.response;
|
|
1415
1434
|
}
|
|
1416
1435
|
// Check for blocking bugs from recordings
|
|
1417
1436
|
const bugCheck = await checkBugBlocking(toolName, validation.data);
|
|
1418
1437
|
if (bugCheck.blocked) {
|
|
1438
|
+
await recordBlockEvent(bugCheck.block, toolName);
|
|
1419
1439
|
return bugCheck.response;
|
|
1420
1440
|
}
|
|
1421
1441
|
// Check for duplicate session (multiple MCPs for same Claude session)
|
|
1422
1442
|
const duplicateInfo = getDuplicateSessionInfo();
|
|
1423
1443
|
const duplicateCheck = checkDuplicateSession(duplicateInfo, toolName);
|
|
1424
1444
|
if (duplicateCheck.blocked) {
|
|
1445
|
+
await recordBlockEvent(duplicateCheck.block, toolName);
|
|
1425
1446
|
return duplicateCheck.response;
|
|
1426
1447
|
}
|
|
1448
|
+
// Every guard passed - a block that recurs later is a new event
|
|
1449
|
+
clearBlockEvents();
|
|
1427
1450
|
// Pass validated data to handler
|
|
1428
1451
|
try {
|
|
1429
1452
|
const result = await tool.handler(validation.data, extra?.signal);
|
|
@@ -1815,8 +1838,16 @@ async function main() {
|
|
|
1815
1838
|
// session that has not registered yet is invisible to that question.
|
|
1816
1839
|
serverClaims.collectDeadSessions();
|
|
1817
1840
|
await serverClaims.registerSession(process.cwd());
|
|
1818
|
-
// Initialize server manager - recover running servers and start auto-run servers
|
|
1819
|
-
|
|
1841
|
+
// Initialize server manager - recover running servers and start auto-run servers.
|
|
1842
|
+
// Tool calls queue behind this (see waitForStartupRecovery); release the gate
|
|
1843
|
+
// even if recovery throws, or every later call would wait out the timeout.
|
|
1844
|
+
let serverInitResult;
|
|
1845
|
+
try {
|
|
1846
|
+
serverInitResult = await serverManager.initialize();
|
|
1847
|
+
}
|
|
1848
|
+
finally {
|
|
1849
|
+
startupGate.markComplete();
|
|
1850
|
+
}
|
|
1820
1851
|
if (serverInitResult.recovered.length > 0) {
|
|
1821
1852
|
console.error(`[devharness] Recovered ${serverInitResult.recovered.length} running server(s): ${serverInitResult.recovered.join(', ')}`);
|
|
1822
1853
|
}
|