@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "private": false,
5
5
  "description": "Workit — workflow rails for agentic coding: specs, plans, YouTrack, CI-gated commits (shared core)",
6
6
  "keywords": [
@@ -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@0.8.5 workit-cursor-mcp "$@"
6
+ exec npx -y --prefer-online --package=@brainervirus/workit-cursor@latest workit-cursor-mcp "$@"
@@ -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. Never reads credential values: only
5
- // existence, mode, and a placeholder flag are evaluated; token bytes never enter
6
- // the report or any log event.
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 { CURSOR_RUNTIME_PACKAGE, cursorHooksEntry, isWorkitPlugin } from "./registration";
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 = { passed: number; warned: number; failed: number; total: number };
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
- offline: true;
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
- // CA-17: exact positional tokens a substring match would accept
423
- // `@latest-alpha` or `workit-cursor-mcp-foo`.
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 { id: "launcher", status: "pass", detail: `${res.host} launcher/hook entries present` };
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 { id: "utility", status: "pass", detail: "git (and flock where required) on PATH" };
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 { id: "stale_pin", status: "pass", detail: "no opencode config — not registered" };
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 { id: "duplicate_registration", status: "pass", detail: "no duplicate registrations" };
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 { id: "malformed_config", status: "pass", detail: "config files parse" };
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 { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
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 { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
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 { id: "credential_metadata", status: "pass", detail: "no credentials configured" };
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 { id: "log_writable", status: "pass", detail: "log directory writable" };
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
- ? { ...c, status: "warn" as const, detail: `${c.detail} (not enforced by installer)` }
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: true,
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: true,
1086
+ offline,
865
1087
  failed: checks.filter((c) => c.status === "fail").map((c) => c.id),
866
1088
  total: checks.length,
867
1089
  });
@@ -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 digest.
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
- menu: { presented: false, chosen: "", evidence: null },
715
- execution: { status: "pending", mode: null, evidence: null },
716
- handoff_destination: false,
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
 
@@ -180,11 +180,13 @@ export function mergeCursorHooks(
180
180
  }
181
181
 
182
182
  /**
183
- * Canonical reviewed pin for the Cursor npm runtime (README "Update review").
184
- * The single source for every source-derived Cursor runtime selector; the
185
- * committed manifests keep the literal (static data cannot import TS).
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@0.8.5";
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: ["-y", `--package=${CURSOR_RUNTIME_PACKAGE}`, "workit-cursor-mcp", "${workspaceFolder}"],
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
  }