@axiomatic-labs/claudeflow 2.32.4 → 2.32.6

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/lib/doctor.js CHANGED
@@ -146,9 +146,43 @@ function checkStaleLockfiles(cwd) {
146
146
  // Detection strategy (in order of preference):
147
147
  // 1. error-observer.json exists → read its `port` + `pid_path`.
148
148
  // 2. If pidfile exists and PID is alive → running=true.
149
- // 3. Else, TCP probe the observer port via `lsof -i :PORT -sTCP:LISTEN -t`.
149
+ // 3. Else, TCP-probe the observer port with a short `net.connect` (via a
150
+ // one-shot `node -e` child, so the synchronous status path stays sync).
150
151
  // The pidfile may be missing (race) but if SOMETHING is listening on
151
- // the configured port, the daemon is up.
152
+ // the configured port, the daemon is up. This replaces a prior `lsof`
153
+ // probe that was slow on macOS (often >1.5s — the timeout — so a live
154
+ // observer read as stopped) and absent on Windows (no `lsof` → the
155
+ // fallback never ran). `net.connect` is a Node built-in: fast and
156
+ // cross-platform. Trade-off: it proves a listener EXISTS but cannot
157
+ // attribute the owning PID, so a probe hit returns `pid: null`.
158
+ //
159
+ // Synchronous TCP liveness check WITHOUT lsof. Node has no sync net.connect,
160
+ // so we run the connect in a one-shot `node -e` child via spawnSync — keeping
161
+ // readObserverState (and its sync callers: getMcpInfo/getObserverInfo/
162
+ // collectStatus) synchronous. Probes both IPv4 and IPv6 loopback (the daemon
163
+ // may bind either); exit 0 = a listener answered. Reached only when the
164
+ // pidfile path already failed, so the ~node-startup cost is off the hot path.
165
+ function portHasListenerSync(port) {
166
+ if (!Number.isFinite(port) || port <= 0) return false;
167
+ const script =
168
+ 'const net=require("net");' +
169
+ 'const port=' + Number(port) + ';' +
170
+ 'const hosts=["127.0.0.1","::1"];' +
171
+ 'let pending=hosts.length,done=false;' +
172
+ 'const finish=(ok)=>{if(done)return;if(ok){done=true;process.exit(0);}if(--pending===0){done=true;process.exit(1);}};' +
173
+ 'for(const h of hosts){const s=net.connect({host:h,port});s.setTimeout(250);' +
174
+ 's.once("connect",()=>{s.destroy();finish(true);});' +
175
+ 's.once("error",()=>finish(false));' +
176
+ 's.once("timeout",()=>{s.destroy();finish(false);});}';
177
+ try {
178
+ const { spawnSync } = require('child_process');
179
+ const r = spawnSync(process.execPath, ['-e', script], { timeout: 1000 });
180
+ return r.status === 0;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+
152
186
  function readObserverState(cwd) {
153
187
  const observerJsonPath = path.join(cwd, '.claudeflow', 'tmp', 'error-observer.json');
154
188
  // Defense-in-depth retry: even though the daemon now writes atomically
@@ -200,18 +234,12 @@ function readObserverState(cwd) {
200
234
  if (alive) return { running: true, pid, port, source: 'pidfile', observerJsonPath };
201
235
  }
202
236
 
203
- // Fallback: TCP probe via lsof. Catches the case where pidfile is missing
204
- // but the daemon process is still listening on the port.
205
- try {
206
- const { spawnSync } = require('child_process');
207
- const r = spawnSync('lsof', ['-i', `:${port}`, '-sTCP:LISTEN', '-t'], { encoding: 'utf8', timeout: 1500 });
208
- if (r.status === 0 && r.stdout && r.stdout.trim()) {
209
- const tcpPid = parseInt(r.stdout.trim().split('\n')[0], 10);
210
- if (Number.isFinite(tcpPid) && tcpPid > 0) {
211
- return { running: true, pid: tcpPid, port, source: 'tcp-probe', observerJsonPath };
212
- }
213
- }
214
- } catch {}
237
+ // Fallback: cross-platform net.connect probe. Catches the case where the
238
+ // pidfile is missing (race / external start) but the daemon is still
239
+ // listening on the port. Can't attribute the PID, so report pid: null.
240
+ if (portHasListenerSync(port)) {
241
+ return { running: true, pid: null, port, source: 'tcp-probe', observerJsonPath };
242
+ }
215
243
 
216
244
  return { running: false, pid: pid, port, source: 'no-listener', observerJsonPath };
217
245
  }
package/lib/panel.js CHANGED
@@ -230,16 +230,32 @@ function getSetupContextInfo(cwd) {
230
230
  try { ctx = JSON.parse(fs.readFileSync(p, 'utf8')); }
231
231
  catch { return { exists: false, path: p }; }
232
232
 
233
- const canonicalToolingTypes = ['unit_test', 'integration_test', 'api_contract_test', 'e2e_test', 'security_test'];
233
+ // Canonical evidence types per test-tooling-schema.md: the 5 binary `_test`
234
+ // types + the 2 observational `_verification` types. (benchmark /
235
+ // exploratory_test are conditional — triggered only by PERFORMANCE_CHANGE or
236
+ // unmeasurable correctness — so they are NOT required for "complete".)
237
+ // Source of truth: .claude/skills/claudeflow-init/references/test-tooling-schema.md.
238
+ const canonicalToolingTypes = [
239
+ 'unit_test',
240
+ 'integration_test',
241
+ 'api_contract_test',
242
+ 'e2e_test',
243
+ 'security_test',
244
+ 'browser_verification',
245
+ 'visual_verification',
246
+ ];
234
247
  const tooling = ctx.test_tooling || {};
235
- const toolingComplete = canonicalToolingTypes.every((t) => {
236
- const e = tooling[t];
237
- return e && typeof e === 'object' && typeof e.command_pattern === 'string' && e.command_pattern.trim();
238
- });
239
- const missingTooling = canonicalToolingTypes.filter((t) => {
240
- const e = tooling[t];
241
- return !(e && typeof e === 'object' && e.command_pattern && e.command_pattern.trim());
242
- });
248
+ // An entry is "defined" when it is shell-backed (`command_pattern`) OR
249
+ // Skill-backed (`invocation`). browser_verification is Skill-backed and
250
+ // legitimately carries NO command_pattern, so a command_pattern-only check
251
+ // would false-flag it as missing.
252
+ const isToolingDefined = (e) =>
253
+ e &&
254
+ typeof e === 'object' &&
255
+ ((typeof e.command_pattern === 'string' && e.command_pattern.trim()) ||
256
+ (typeof e.invocation === 'string' && e.invocation.trim()));
257
+ const toolingComplete = canonicalToolingTypes.every((t) => isToolingDefined(tooling[t]));
258
+ const missingTooling = canonicalToolingTypes.filter((t) => !isToolingDefined(tooling[t]));
243
259
 
244
260
  return {
245
261
  exists: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.32.4",
3
+ "version": "2.32.6",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"