@brainervirus/workit-core 0.8.6 → 0.8.8
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/package.json +1 -1
- package/scripts/doctor-check.ts +16 -0
- package/scripts/install-cursor-plugin.sh +16 -0
- package/scripts/run-cursor-mcp.sh +1 -1
- package/src/core/doctor.ts +245 -23
- package/src/core/flow-state.ts +7 -4
- package/src/core/registration.ts +14 -6
package/package.json
CHANGED
package/scripts/doctor-check.ts
CHANGED
|
@@ -3,11 +3,27 @@
|
|
|
3
3
|
// return nonzero when a required registration/config check fails. `installer:
|
|
4
4
|
// true` limits the enforced checks to the ones the installer just guaranteed, so
|
|
5
5
|
// a dev/stub checkout without built assets does not fail a post-install run.
|
|
6
|
+
// CA-02: `cursor --stale` is the installer's self-heal pre-check — it exits 2
|
|
7
|
+
// when the doctor's stale_install check fails and 0 otherwise. A
|
|
8
|
+
// registry-unreachable comparison warns as registry_unreachable and is NOT
|
|
9
|
+
// stale (CA-04): no false stale_install and no install failure. Other check
|
|
10
|
+
// failures are left to the post-install gate.
|
|
6
11
|
import { runDoctor } from "../src/core/doctor";
|
|
7
12
|
|
|
8
13
|
const host = process.argv[2] === "cursor" ? "cursor" : "opencode";
|
|
14
|
+
const staleOnly = process.argv.includes("--stale");
|
|
9
15
|
const report = runDoctor({ host, installer: true });
|
|
10
16
|
|
|
17
|
+
if (staleOnly) {
|
|
18
|
+
const stale = report.checks.find((c) => c.id === "stale_install");
|
|
19
|
+
if (stale?.status === "fail") {
|
|
20
|
+
process.stderr.write(`STALE: ${stale.detail}\n`);
|
|
21
|
+
if (stale.fix) process.stderr.write(` fix: ${stale.fix}\n`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
if (report.exitCode !== 0) {
|
|
12
28
|
for (const check of report.checks) {
|
|
13
29
|
if (check.status === "fail") {
|
|
@@ -43,6 +43,22 @@ else
|
|
|
43
43
|
ROOT="$LOCAL_ROOT"
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
|
+
# CA-02: self-heal pre-check. The doctor's stale_install check runs against the
|
|
47
|
+
# INSTALLED plugin dir (version/selectors vs the current runtime). Exit 2 means
|
|
48
|
+
# stale: the refresh below syncs the plugin dir from ROOT and the registration
|
|
49
|
+
# pass rewrites the workit MCP/hook entries to the canonical current selector.
|
|
50
|
+
# The check reads only workit-owned files, so a healthy install is byte-untouched;
|
|
51
|
+
# a registry-unreachable comparison reports registry_unreachable, never stale
|
|
52
|
+
# (CA-04) — no false stale_install and no install failure.
|
|
53
|
+
if bun "$ROOT/packages/workit-core/scripts/doctor-check.ts" cursor --stale; then
|
|
54
|
+
:
|
|
55
|
+
elif [ $? -eq 2 ]; then
|
|
56
|
+
echo "workit: stale Cursor plugin install detected — self-healing (refresh + canonical re-registration)" >&2
|
|
57
|
+
else
|
|
58
|
+
echo "FATAL: pre-install doctor probe failed" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
46
62
|
chmod +x "$ROOT/packages/workit-core/scripts/sync-runtime.sh" "$ROOT/packages/workit-core/scripts/"*.sh
|
|
47
63
|
# Prefer syncing from this ROOT (dev or freshly cloned share)
|
|
48
64
|
export WORKFLOW_TOOLKIT_DEV="$ROOT"
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
# Launch the published package's MCP bin through npx, never a repo-relative
|
|
4
4
|
# dist or share clone; a startup/network failure surfaces via npx's nonzero exit.
|
|
5
5
|
set -euo pipefail
|
|
6
|
-
exec npx -y --package=@brainervirus/workit-cursor@
|
|
6
|
+
exec npx -y --prefer-online --package=@brainervirus/workit-cursor@latest workit-cursor-mcp "$@"
|
package/src/core/doctor.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Shared offline doctor (DG-07/DG-08, CA-09). One host-neutral engine checks the
|
|
2
2
|
// installed Workit surfaces — pins, versions, assets, launchers, runtimes,
|
|
3
3
|
// utilities, registrations, config, workspace match, credential metadata, and
|
|
4
|
-
// log writability — with no network access
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// log writability — with no network access except the optional registry probe
|
|
5
|
+
// behind the stale-install comparison (CA-04), which fails open. Never reads
|
|
6
|
+
// credential values: only existence, mode, and a placeholder flag are evaluated;
|
|
7
|
+
// token bytes never enter the report or any log event.
|
|
7
8
|
import { spawnSync } from "node:child_process";
|
|
8
9
|
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
10
|
import os from "node:os";
|
|
@@ -11,7 +12,13 @@ import path from "node:path";
|
|
|
11
12
|
import { SUPPORT_MATRIX } from "./support-matrix";
|
|
12
13
|
import { EVENT } from "./boundary";
|
|
13
14
|
import { getDiagnosticLogger, isConfigObject } from "./config";
|
|
14
|
-
import {
|
|
15
|
+
import { packageRoot } from "./package-root";
|
|
16
|
+
import {
|
|
17
|
+
CURSOR_RUNTIME_PACKAGE,
|
|
18
|
+
cursorHooksEntry,
|
|
19
|
+
cursorMcpServerEntry,
|
|
20
|
+
isWorkitPlugin,
|
|
21
|
+
} from "./registration";
|
|
15
22
|
import { resolveWorkspaceFrom } from "./workspaces";
|
|
16
23
|
import { validateCursorSkills } from "./skill-manifests";
|
|
17
24
|
|
|
@@ -28,6 +35,8 @@ export type DoctorCheckId =
|
|
|
28
35
|
| "launcher"
|
|
29
36
|
| "utility"
|
|
30
37
|
| "stale_pin"
|
|
38
|
+
| "stale_install"
|
|
39
|
+
| "registry_unreachable"
|
|
31
40
|
| "duplicate_registration"
|
|
32
41
|
| "malformed_config"
|
|
33
42
|
| "workspace_mismatch"
|
|
@@ -46,12 +55,18 @@ export type DoctorCheck = {
|
|
|
46
55
|
|
|
47
56
|
export type DoctorFix = { id: DoctorCheckId; fix: string };
|
|
48
57
|
|
|
49
|
-
export type DoctorSummary = {
|
|
58
|
+
export type DoctorSummary = {
|
|
59
|
+
passed: number;
|
|
60
|
+
warned: number;
|
|
61
|
+
failed: number;
|
|
62
|
+
total: number;
|
|
63
|
+
};
|
|
50
64
|
|
|
51
65
|
export type DoctorReport = {
|
|
52
66
|
ok: boolean;
|
|
53
67
|
exitCode: number;
|
|
54
|
-
|
|
68
|
+
/** False when the doctor consulted the npm registry (local-dist version probe). */
|
|
69
|
+
offline: boolean;
|
|
55
70
|
host: DoctorHost;
|
|
56
71
|
checked_at: string;
|
|
57
72
|
summary: DoctorSummary;
|
|
@@ -418,12 +433,13 @@ const registeredCursorLauncher = (res: Resolved): CursorLauncher | null | "inval
|
|
|
418
433
|
const executable = path.basename(command).toLowerCase();
|
|
419
434
|
// CA-17: the canonical launcher runs the published package through npx; the
|
|
420
435
|
// offline doctor validates its shape (never the registry reachability).
|
|
436
|
+
// Exact positional tokens — a substring match would accept `@latest-alpha`
|
|
437
|
+
// or `workit-cursor-mcp-foo`, and any extra/missing token would weaken the
|
|
438
|
+
// freshness guarantee (`--prefer-online` is mandatory).
|
|
439
|
+
const canonical = cursorMcpServerEntry("").args;
|
|
421
440
|
if (executable === "npx" || executable === "npx.exe" || executable === "npx.cmd") {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (args[0] !== "-y") return "invalid";
|
|
425
|
-
if (args[1] !== `--package=${CURSOR_RUNTIME_PACKAGE}`) return "invalid";
|
|
426
|
-
if (args[2] !== "workit-cursor-mcp") return "invalid";
|
|
441
|
+
if (args.length !== canonical.length || args.some((a, i) => a !== canonical[i]))
|
|
442
|
+
return "invalid";
|
|
427
443
|
return { kind: "npx" };
|
|
428
444
|
}
|
|
429
445
|
if (executable !== "node" && executable !== "node.exe") return "invalid";
|
|
@@ -533,7 +549,11 @@ const checkLauncher = (res: Resolved): DoctorCheck => {
|
|
|
533
549
|
detail: "no dev checkout found (WORKFLOW_TOOLKIT_DEV) — skipping non-Cursor launcher checks",
|
|
534
550
|
};
|
|
535
551
|
}
|
|
536
|
-
return {
|
|
552
|
+
return {
|
|
553
|
+
id: "launcher",
|
|
554
|
+
status: "pass",
|
|
555
|
+
detail: `${res.host} launcher/hook entries present`,
|
|
556
|
+
};
|
|
537
557
|
};
|
|
538
558
|
|
|
539
559
|
const checkUtility = (res: Resolved): DoctorCheck => {
|
|
@@ -554,7 +574,11 @@ const checkUtility = (res: Resolved): DoctorCheck => {
|
|
|
554
574
|
fix: "Install util-linux (flock)",
|
|
555
575
|
};
|
|
556
576
|
}
|
|
557
|
-
return {
|
|
577
|
+
return {
|
|
578
|
+
id: "utility",
|
|
579
|
+
status: "pass",
|
|
580
|
+
detail: "git (and flock where required) on PATH",
|
|
581
|
+
};
|
|
558
582
|
};
|
|
559
583
|
|
|
560
584
|
const staleEntry = (entry: string): "ok" | "stale" | "missing-file" => {
|
|
@@ -578,7 +602,11 @@ const checkStalePin = (res: Resolved): DoctorCheck => {
|
|
|
578
602
|
};
|
|
579
603
|
}
|
|
580
604
|
if (!existsSync(res.opencodeConfig)) {
|
|
581
|
-
return {
|
|
605
|
+
return {
|
|
606
|
+
id: "stale_pin",
|
|
607
|
+
status: "pass",
|
|
608
|
+
detail: "no opencode config — not registered",
|
|
609
|
+
};
|
|
582
610
|
}
|
|
583
611
|
const cfg = readJson(res.opencodeConfig);
|
|
584
612
|
const entries = pluginEntries(cfg);
|
|
@@ -610,6 +638,169 @@ const checkStalePin = (res: Resolved): DoctorCheck => {
|
|
|
610
638
|
return { id: "stale_pin", status: "pass", detail: "opencode pin resolves" };
|
|
611
639
|
};
|
|
612
640
|
|
|
641
|
+
// Cursor plugin stale-install detection (CA-01/CA-04). Compares the installed
|
|
642
|
+
// `~/.cursor/plugins/local/workit` against the current runtime source of truth
|
|
643
|
+
// (`CURSOR_RUNTIME_PACKAGE` + the canonical entry builders from registration.ts,
|
|
644
|
+
// D-02) and surfaces a structured `stale_install` finding with the exact repair
|
|
645
|
+
// step. A stale install means the plugin's sessionStart hook and MCP entry run
|
|
646
|
+
// an ancient runtime that no longer auto-registers the workflow features.
|
|
647
|
+
//
|
|
648
|
+
// Three independent staleness signals are read from the installed plugin dir:
|
|
649
|
+
// 1. Legacy selectors (offline evidence, checked first): the plugin's own
|
|
650
|
+
// mcp.json (`--package=...workit-cursor@<exact>` pins other than the
|
|
651
|
+
// canonical `@latest`) and hooks-cursor.json sessionStart command.
|
|
652
|
+
// 2. Installed version behind the current source release (offline): the dev
|
|
653
|
+
// checkout's workit-cursor version (or the doctor's own package version)
|
|
654
|
+
// is the release train the doctor ships with.
|
|
655
|
+
// 3. Local-dist installs behind the published runtime (needs the registry):
|
|
656
|
+
// the probe is the sole network read in the doctor. A SUCCEEDED probe with
|
|
657
|
+
// the installed version genuinely behind the published runtime is a hard
|
|
658
|
+
// `stale_install` fail (the installer self-heals on it); an unreachable
|
|
659
|
+
// registry fails open (CA-04) as a `registry_unreachable` warning, never a
|
|
660
|
+
// false `stale_install` and never a hard doctor failure. Canonical
|
|
661
|
+
// `@latest` installs skip the probe entirely: the selector resolves fresh
|
|
662
|
+
// at launch, so the installed package.json version is metadata, not a
|
|
663
|
+
// freshness signal.
|
|
664
|
+
// Test seams (no spawns on the canonical path): WORKIT_DOCTOR_STALE_REGISTRY_VERSION
|
|
665
|
+
// short-circuits the probe with the resolved latest version; the optional
|
|
666
|
+
// WORKIT_DOCTOR_STALE_REGISTRY_CMD replaces the `npm view` binary (a
|
|
667
|
+
// nonexistent path deterministically exercises the fail-open path).
|
|
668
|
+
const pluginMcpSelectors = (res: Resolved): string[] | null => {
|
|
669
|
+
const p = path.join(res.cursorPluginDir, "mcp.json");
|
|
670
|
+
if (!existsSync(p)) return null;
|
|
671
|
+
const cfg = readJson(p);
|
|
672
|
+
const server = cfg?.mcpServers?.workit;
|
|
673
|
+
if (!server || typeof server !== "object" || Array.isArray(server)) return null;
|
|
674
|
+
const args = server.args;
|
|
675
|
+
if (!Array.isArray(args)) return null;
|
|
676
|
+
return args.map(String).filter((a) => a.startsWith("--package="));
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const registryLatestVersion = (res: Resolved): string | null => {
|
|
680
|
+
const seam = res.env.WORKIT_DOCTOR_STALE_REGISTRY_VERSION;
|
|
681
|
+
if (typeof seam === "string" && seam) {
|
|
682
|
+
return /^\d+(?:\.\d+){1,2}$/.test(seam) ? seam : null;
|
|
683
|
+
}
|
|
684
|
+
const cmd =
|
|
685
|
+
res.env.WORKIT_DOCTOR_STALE_REGISTRY_CMD ?? (commandOnPath("npm", res.env) ? "npm" : null);
|
|
686
|
+
if (!cmd) return null;
|
|
687
|
+
try {
|
|
688
|
+
const r = spawnSync(cmd, ["view", CURSOR_RUNTIME_PACKAGE, "version"], {
|
|
689
|
+
encoding: "utf8",
|
|
690
|
+
timeout: 20_000,
|
|
691
|
+
env: res.env,
|
|
692
|
+
});
|
|
693
|
+
const v = (r.stdout ?? "").trim().split(/\s+/)[0];
|
|
694
|
+
return r.status === 0 && /^\d+(?:\.\d+){1,2}$/.test(v) ? v : null;
|
|
695
|
+
} catch {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const installedPluginVersion = (res: Resolved): string | null => {
|
|
701
|
+
const pkg = readJson(path.join(res.cursorPluginDir, "package.json"));
|
|
702
|
+
return typeof pkg?.version === "string" && pkg.version ? pkg.version : null;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const checkStaleInstall = (res: Resolved): DoctorCheck & { registryProbed?: boolean } => {
|
|
706
|
+
if (res.host !== "cursor" && res.host !== "cli") {
|
|
707
|
+
return {
|
|
708
|
+
id: "stale_install",
|
|
709
|
+
status: "pass",
|
|
710
|
+
detail: "cursor plugin not inspected on the opencode host",
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const canonicalMcp = cursorMcpServerEntry("").args.find((a) => a.startsWith("--package="));
|
|
714
|
+
const mcpSelectors = pluginMcpSelectors(res);
|
|
715
|
+
const legacyPin = (mcpSelectors ?? []).find(
|
|
716
|
+
(s) => s !== canonicalMcp && s.includes("@brainervirus/workit-cursor@"),
|
|
717
|
+
);
|
|
718
|
+
if (legacyPin) {
|
|
719
|
+
return {
|
|
720
|
+
id: "stale_install",
|
|
721
|
+
status: "fail",
|
|
722
|
+
detail: `stale_install: plugin mcp.json pins a legacy selector ${legacyPin} (canonical: ${canonicalMcp})`,
|
|
723
|
+
fix: "Re-run install-cursor-plugin.sh — it rewrites the workit MCP entry to the canonical @latest selector",
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const hooksFile = path.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
|
|
727
|
+
const hookCmd =
|
|
728
|
+
(readJson(hooksFile)?.hooks?.sessionStart?.[0]?.command as string | undefined) ?? null;
|
|
729
|
+
const canonicalHook = canonicalCursorHook;
|
|
730
|
+
const staleHook =
|
|
731
|
+
hookCmd !== null &&
|
|
732
|
+
hookCmd !== canonicalHook &&
|
|
733
|
+
!hookCmd.startsWith("node ") &&
|
|
734
|
+
hookCmd.includes("@brainervirus/workit-cursor@");
|
|
735
|
+
if (staleHook) {
|
|
736
|
+
return {
|
|
737
|
+
id: "stale_install",
|
|
738
|
+
status: "fail",
|
|
739
|
+
detail: `stale_install: sessionStart hook runs a legacy selector (canonical: ${canonicalHook})`,
|
|
740
|
+
fix: "Re-run install-cursor-plugin.sh — it rewrites the sessionStart hook to the canonical @latest selector",
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
const installed = installedPluginVersion(res);
|
|
744
|
+
const source = res.dev
|
|
745
|
+
? ((readJson(path.join(res.dev, "packages/workit-cursor/package.json"))?.version as
|
|
746
|
+
| string
|
|
747
|
+
| undefined) ?? null)
|
|
748
|
+
: ((readJson(path.join(packageRoot(), "package.json"))?.version as string | undefined) ?? null);
|
|
749
|
+
// A local-dist install (node entry) runs the installed dir's own dist, so its
|
|
750
|
+
// version is comparable against the current runtime (and the published one
|
|
751
|
+
// below). Canonical @latest installs resolve fresh at launch — the installed
|
|
752
|
+
// package.json version is metadata, not a freshness signal (CA-04), so they
|
|
753
|
+
// skip both version comparisons and never fail stale_install on metadata.
|
|
754
|
+
const localDist = mcpSelectors === null && hookCmd !== null && hookCmd.startsWith("node ");
|
|
755
|
+
if (localDist && installed !== null && source !== null && !semverAtLeast(installed, source)) {
|
|
756
|
+
return {
|
|
757
|
+
id: "stale_install",
|
|
758
|
+
status: "fail",
|
|
759
|
+
detail: `stale_install: installed workit-cursor ${installed} is behind the current runtime ${source}`,
|
|
760
|
+
fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory and rewrites the workit MCP/hook entries",
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
if (installed !== null && localDist) {
|
|
764
|
+
const expected = registryLatestVersion(res);
|
|
765
|
+
if (expected === null) {
|
|
766
|
+
return {
|
|
767
|
+
id: "registry_unreachable",
|
|
768
|
+
status: "warn",
|
|
769
|
+
registryProbed: true,
|
|
770
|
+
detail:
|
|
771
|
+
"registry_unreachable: cannot compare installed workit-cursor against the published runtime",
|
|
772
|
+
fix: "Retry when the npm registry is reachable, or re-run install-cursor-plugin.sh to refresh the install",
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
if (!semverAtLeast(installed, expected)) {
|
|
776
|
+
// The probe SUCCEEDED and the installed version is genuinely behind the
|
|
777
|
+
// published runtime: report a hard fail so `doctor-check.ts cursor --stale`
|
|
778
|
+
// exits 2 and the installer self-heals the local-dist install.
|
|
779
|
+
return {
|
|
780
|
+
id: "stale_install",
|
|
781
|
+
status: "fail",
|
|
782
|
+
registryProbed: true,
|
|
783
|
+
detail: `stale_install: local-dist workit-cursor ${installed} is behind the published runtime ${expected}`,
|
|
784
|
+
fix: "Re-run install-cursor-plugin.sh — it refreshes the plugin directory with the current build",
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
id: "stale_install",
|
|
789
|
+
status: "pass",
|
|
790
|
+
registryProbed: true,
|
|
791
|
+
detail: `installed local-dist workit-cursor ${installed} matches the published runtime ${expected}`,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
id: "stale_install",
|
|
796
|
+
status: "pass",
|
|
797
|
+
detail:
|
|
798
|
+
installed === null
|
|
799
|
+
? "installed workit-cursor selectors are canonical"
|
|
800
|
+
: `installed workit-cursor ${installed} is metadata on the canonical @latest install (fresh at launch)`,
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
|
|
613
804
|
const checkDuplicateRegistration = (res: Resolved): DoctorCheck => {
|
|
614
805
|
const problems: string[] = [];
|
|
615
806
|
const opencodeHost = res.host !== "cursor";
|
|
@@ -658,7 +849,11 @@ const checkDuplicateRegistration = (res: Resolved): DoctorCheck => {
|
|
|
658
849
|
}
|
|
659
850
|
}
|
|
660
851
|
if (problems.length === 0) {
|
|
661
|
-
return {
|
|
852
|
+
return {
|
|
853
|
+
id: "duplicate_registration",
|
|
854
|
+
status: "pass",
|
|
855
|
+
detail: "no duplicate registrations",
|
|
856
|
+
};
|
|
662
857
|
}
|
|
663
858
|
return {
|
|
664
859
|
id: "duplicate_registration",
|
|
@@ -681,7 +876,11 @@ const checkMalformedConfig = (res: Resolved): DoctorCheck => {
|
|
|
681
876
|
if (cursorHost && existsSync(res.cursorMcp)) files.push(res.cursorMcp);
|
|
682
877
|
const bad = files.filter((p) => !parsesAsConfigObject(p));
|
|
683
878
|
if (bad.length === 0)
|
|
684
|
-
return {
|
|
879
|
+
return {
|
|
880
|
+
id: "malformed_config",
|
|
881
|
+
status: "pass",
|
|
882
|
+
detail: "config files parse",
|
|
883
|
+
};
|
|
685
884
|
return {
|
|
686
885
|
id: "malformed_config",
|
|
687
886
|
status: "fail",
|
|
@@ -693,10 +892,18 @@ const checkMalformedConfig = (res: Resolved): DoctorCheck => {
|
|
|
693
892
|
const checkWorkspaceMismatch = (res: Resolved): DoctorCheck => {
|
|
694
893
|
const file = path.join(res.configDir, "workspaces.json");
|
|
695
894
|
if (!existsSync(file))
|
|
696
|
-
return {
|
|
895
|
+
return {
|
|
896
|
+
id: "workspace_mismatch",
|
|
897
|
+
status: "pass",
|
|
898
|
+
detail: "no workspaces configured",
|
|
899
|
+
};
|
|
697
900
|
const ws = readJson(file);
|
|
698
901
|
if (!Array.isArray(ws?.workspaces)) {
|
|
699
|
-
return {
|
|
902
|
+
return {
|
|
903
|
+
id: "workspace_mismatch",
|
|
904
|
+
status: "pass",
|
|
905
|
+
detail: "no workspaces configured",
|
|
906
|
+
};
|
|
700
907
|
}
|
|
701
908
|
const match = resolveWorkspaceFrom(res.cwd, res.configDir);
|
|
702
909
|
if (match)
|
|
@@ -747,7 +954,11 @@ const checkCredentialMetadata = (res: Resolved): DoctorCheck => {
|
|
|
747
954
|
}
|
|
748
955
|
|
|
749
956
|
if (tokenPaths.length === 0) {
|
|
750
|
-
return {
|
|
957
|
+
return {
|
|
958
|
+
id: "credential_metadata",
|
|
959
|
+
status: "pass",
|
|
960
|
+
detail: "no credentials configured",
|
|
961
|
+
};
|
|
751
962
|
}
|
|
752
963
|
const problems: string[] = [];
|
|
753
964
|
for (const raw of tokenPaths) {
|
|
@@ -785,7 +996,11 @@ const checkLogWritable = (res: Resolved): DoctorCheck => {
|
|
|
785
996
|
try {
|
|
786
997
|
mkdirSync(logsDir, { recursive: true, mode: 0o700 });
|
|
787
998
|
writeFileSync(probe, '{"probe":true}\n', { mode: 0o600 });
|
|
788
|
-
return {
|
|
999
|
+
return {
|
|
1000
|
+
id: "log_writable",
|
|
1001
|
+
status: "pass",
|
|
1002
|
+
detail: "log directory writable",
|
|
1003
|
+
};
|
|
789
1004
|
} catch (err) {
|
|
790
1005
|
return {
|
|
791
1006
|
id: "log_writable",
|
|
@@ -809,6 +1024,7 @@ const RUN_CHECKS: Array<(res: Resolved) => DoctorCheck> = [
|
|
|
809
1024
|
checkLauncher,
|
|
810
1025
|
checkUtility,
|
|
811
1026
|
checkStalePin,
|
|
1027
|
+
checkStaleInstall,
|
|
812
1028
|
checkDuplicateRegistration,
|
|
813
1029
|
checkMalformedConfig,
|
|
814
1030
|
checkWorkspaceMismatch,
|
|
@@ -826,6 +1042,7 @@ const INSTALLER_REQUIRED = new Set<DoctorCheckId>([
|
|
|
826
1042
|
"launcher",
|
|
827
1043
|
"utility",
|
|
828
1044
|
"stale_pin",
|
|
1045
|
+
"stale_install",
|
|
829
1046
|
"duplicate_registration",
|
|
830
1047
|
"malformed_config",
|
|
831
1048
|
]);
|
|
@@ -836,7 +1053,11 @@ export const runDoctor = (options: DoctorOptions = {}): DoctorReport => {
|
|
|
836
1053
|
const checks = res.installer
|
|
837
1054
|
? raw.map((c) =>
|
|
838
1055
|
c.status === "fail" && !INSTALLER_REQUIRED.has(c.id)
|
|
839
|
-
? {
|
|
1056
|
+
? {
|
|
1057
|
+
...c,
|
|
1058
|
+
status: "warn" as const,
|
|
1059
|
+
detail: `${c.detail} (not enforced by installer)`,
|
|
1060
|
+
}
|
|
840
1061
|
: c,
|
|
841
1062
|
)
|
|
842
1063
|
: raw;
|
|
@@ -845,10 +1066,11 @@ export const runDoctor = (options: DoctorOptions = {}): DoctorReport => {
|
|
|
845
1066
|
const warned = checks.filter((c) => c.status === "warn").length;
|
|
846
1067
|
const passed = checks.filter((c) => c.status === "pass").length;
|
|
847
1068
|
const exitCode = failed > 0 ? 1 : 0;
|
|
1069
|
+
const offline = !raw.some((c) => "registryProbed" in c && c.registryProbed);
|
|
848
1070
|
const report: DoctorReport = {
|
|
849
1071
|
ok: failed === 0,
|
|
850
1072
|
exitCode,
|
|
851
|
-
offline
|
|
1073
|
+
offline,
|
|
852
1074
|
host: res.host,
|
|
853
1075
|
checked_at: new Date().toISOString(),
|
|
854
1076
|
summary: { passed, warned, failed, total: checks.length },
|
|
@@ -861,7 +1083,7 @@ export const runDoctor = (options: DoctorOptions = {}): DoctorReport => {
|
|
|
861
1083
|
getDiagnosticLogger()?.info(EVENT.doctor, {
|
|
862
1084
|
host: res.host,
|
|
863
1085
|
exit_code: exitCode,
|
|
864
|
-
offline
|
|
1086
|
+
offline,
|
|
865
1087
|
failed: checks.filter((c) => c.status === "fail").map((c) => c.id),
|
|
866
1088
|
total: checks.length,
|
|
867
1089
|
});
|
package/src/core/flow-state.ts
CHANGED
|
@@ -696,7 +696,8 @@ const readCanonicalDigest = (root: string, rel: string): CanonicalDigestResult =
|
|
|
696
696
|
* Approval-integrity reconciliation (CA-02, CA-03): recompute approved
|
|
697
697
|
* document digests in spec-before-plan order and return the reset state plus
|
|
698
698
|
* the structured drift reasons. Spec drift resets the whole approval chain;
|
|
699
|
-
* plan drift (spec valid) preserves the spec approval and
|
|
699
|
+
* plan drift (spec valid) preserves the spec approval/digest and the execution
|
|
700
|
+
* lifecycle, resetting only the plan's approval digest.
|
|
700
701
|
*/
|
|
701
702
|
const resetForSpecDrift = (state: FlowState): FlowState => ({
|
|
702
703
|
...state,
|
|
@@ -711,9 +712,11 @@ const resetForSpecDrift = (state: FlowState): FlowState => ({
|
|
|
711
712
|
const resetForPlanDrift = (state: FlowState): FlowState => ({
|
|
712
713
|
...state,
|
|
713
714
|
plan: { ...state.plan, status: "draft", evidence: null, approved_digest: null },
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
715
|
+
// A plan edit resets only the plan's approval digest (fresh re-approval is
|
|
716
|
+
// required before any plan-gated transition). The execution lifecycle, the
|
|
717
|
+
// recorded menu choice, and the handoff context are lifecycle facts, not
|
|
718
|
+
// plan-approval facts: an in-progress or completed run must not be rewound
|
|
719
|
+
// to pending by a doc edit made during or after implementation.
|
|
717
720
|
updated_at: Date.now(),
|
|
718
721
|
});
|
|
719
722
|
|
package/src/core/registration.ts
CHANGED
|
@@ -180,11 +180,13 @@ export function mergeCursorHooks(
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Canonical
|
|
184
|
-
* The single source for every source-derived Cursor
|
|
185
|
-
* committed manifests keep the literal (static data
|
|
183
|
+
* Canonical selector for the Cursor npm runtime: `@latest` with `--prefer-online`
|
|
184
|
+
* (README "Update review"). The single source for every source-derived Cursor
|
|
185
|
+
* runtime selector; the committed manifests keep the literal (static data
|
|
186
|
+
* cannot import TS). `--prefer-online` is mandatory — it forces npx to check
|
|
187
|
+
* the registry so a stale cached `latest` resolution is never reused.
|
|
186
188
|
*/
|
|
187
|
-
export const CURSOR_RUNTIME_PACKAGE = "@brainervirus/workit-cursor@
|
|
189
|
+
export const CURSOR_RUNTIME_PACKAGE = "@brainervirus/workit-cursor@latest";
|
|
188
190
|
|
|
189
191
|
/**
|
|
190
192
|
* Portable Cursor MCP server entry (CA-16/CA-17): launch the published package
|
|
@@ -197,7 +199,13 @@ export function cursorMcpServerEntry(_packageDir: string): {
|
|
|
197
199
|
} {
|
|
198
200
|
return {
|
|
199
201
|
command: "npx",
|
|
200
|
-
args: [
|
|
202
|
+
args: [
|
|
203
|
+
"-y",
|
|
204
|
+
"--prefer-online",
|
|
205
|
+
`--package=${CURSOR_RUNTIME_PACKAGE}`,
|
|
206
|
+
"workit-cursor-mcp",
|
|
207
|
+
"${workspaceFolder}",
|
|
208
|
+
],
|
|
201
209
|
};
|
|
202
210
|
}
|
|
203
211
|
|
|
@@ -210,7 +218,7 @@ export function cursorHooksEntry(_packageDir: string): {
|
|
|
210
218
|
args: string[];
|
|
211
219
|
} {
|
|
212
220
|
return {
|
|
213
|
-
command: `npx -y --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-session-start`,
|
|
221
|
+
command: `npx -y --prefer-online --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-session-start`,
|
|
214
222
|
args: [],
|
|
215
223
|
};
|
|
216
224
|
}
|