safeword 0.57.0 → 0.58.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.
Files changed (77) hide show
  1. package/dist/{check-IV6KC65F.js → check-6IHMMAS6.js} +34 -26
  2. package/dist/check-6IHMMAS6.js.map +1 -0
  3. package/dist/{chunk-D5H7VBXQ.js → chunk-DIJ35ZXH.js} +2 -2
  4. package/dist/chunk-LRYWFRPD.js +46 -0
  5. package/dist/chunk-LRYWFRPD.js.map +1 -0
  6. package/dist/{chunk-O3QF6QHX.js → chunk-Q7ALQS7T.js} +2 -2
  7. package/dist/{chunk-HGOG5ZLC.js → chunk-UYTOLZT2.js} +115 -29
  8. package/dist/chunk-UYTOLZT2.js.map +1 -0
  9. package/dist/cli.js +8 -8
  10. package/dist/cli.js.map +1 -1
  11. package/dist/{diff-55Y2SH4U.js → diff-5YTHPOAV.js} +36 -5
  12. package/dist/diff-5YTHPOAV.js.map +1 -0
  13. package/dist/{reset-FZRYTUFF.js → reset-OBM2VJQT.js} +2 -2
  14. package/dist/{setup-WKFBBSLJ.js → setup-ZFQDI5QT.js} +4 -4
  15. package/dist/{test-plan-HWRDWG2X.js → test-plan-MS6HRVBR.js} +9 -3
  16. package/dist/test-plan-MS6HRVBR.js.map +1 -0
  17. package/dist/{ticket-new-GXYCW5ML.js → ticket-new-S6IMADXY.js} +6 -2
  18. package/dist/{ticket-new-GXYCW5ML.js.map → ticket-new-S6IMADXY.js.map} +1 -1
  19. package/dist/{upgrade-MCBH6GTD.js → upgrade-N3WTG3KL.js} +6 -9
  20. package/dist/upgrade-N3WTG3KL.js.map +1 -0
  21. package/package.json +12 -11
  22. package/templates/codex/config.toml +3 -3
  23. package/templates/commands/verify.md +7 -1
  24. package/templates/cursor/rules/bdd-tdd.mdc +1 -1
  25. package/templates/cursor/rules/safeword-tdd-review.mdc +1 -1
  26. package/templates/guides/llm-evals-guide.md +485 -0
  27. package/templates/guides/testing-guide.md +35 -13
  28. package/templates/guides/verification-lanes-guide.md +574 -0
  29. package/templates/hooks/cursor/before-shell-execution.ts +20 -0
  30. package/templates/hooks/cursor/gate-adapter.ts +82 -12
  31. package/templates/hooks/cursor/post-tool-quality.ts +1 -1
  32. package/templates/hooks/cursor/pre-tool-quality.ts +8 -2
  33. package/templates/hooks/cursor/stop.ts +17 -0
  34. package/templates/hooks/lib/architecture-staged-scope.ts +130 -0
  35. package/templates/hooks/lib/auto-upgrade-lock.ts +89 -0
  36. package/templates/hooks/lib/auto-upgrade.ts +417 -0
  37. package/templates/hooks/lib/branch-staleness.ts +49 -0
  38. package/templates/hooks/lib/checkbox-transitions.ts +2 -2
  39. package/templates/hooks/lib/cursor-run-identity.ts +216 -0
  40. package/templates/hooks/lib/dependency-readiness.ts +83 -0
  41. package/templates/hooks/lib/done-gate.ts +9 -6
  42. package/templates/hooks/lib/ledger-git.ts +92 -0
  43. package/templates/hooks/lib/ledger-validation.ts +31 -24
  44. package/templates/hooks/lib/quality-state.ts +3 -7
  45. package/templates/hooks/lib/review-trigger.ts +4 -31
  46. package/templates/hooks/lib/safeword-context.ts +83 -0
  47. package/templates/hooks/post-tool-dependency-readiness.ts +84 -0
  48. package/templates/hooks/post-tool-quality.ts +6 -18
  49. package/templates/hooks/pre-tool-architecture-stage.ts +9 -0
  50. package/templates/hooks/pre-tool-quality.ts +22 -4
  51. package/templates/hooks/pre-tool-stale-main.ts +83 -0
  52. package/templates/hooks/record-skill-invocation.ts +32 -6
  53. package/templates/hooks/session-auto-upgrade.ts +13 -300
  54. package/templates/hooks/session-codex-start.ts +39 -0
  55. package/templates/hooks/session-cursor-auto-upgrade.ts +35 -0
  56. package/templates/hooks/session-dependency-readiness.ts +36 -0
  57. package/templates/hooks/session-safeword-context.ts +19 -81
  58. package/templates/hooks/stop-quality.ts +14 -32
  59. package/templates/skills/bdd/SCENARIOS.md +1 -1
  60. package/templates/skills/bdd/TDD.md +3 -1
  61. package/templates/skills/quality-review/SKILL.md +5 -0
  62. package/templates/skills/review-spec/SKILL.md +2 -1
  63. package/templates/skills/tdd-review/SKILL.md +4 -2
  64. package/templates/skills/testing/SKILL.md +33 -0
  65. package/templates/skills/ticket-system/SKILL.md +14 -2
  66. package/templates/skills/verify/SKILL.md +7 -1
  67. package/dist/check-IV6KC65F.js.map +0 -1
  68. package/dist/chunk-FJYRWU2V.js +0 -21
  69. package/dist/chunk-FJYRWU2V.js.map +0 -1
  70. package/dist/chunk-HGOG5ZLC.js.map +0 -1
  71. package/dist/diff-55Y2SH4U.js.map +0 -1
  72. package/dist/test-plan-HWRDWG2X.js.map +0 -1
  73. package/dist/upgrade-MCBH6GTD.js.map +0 -1
  74. /package/dist/{chunk-D5H7VBXQ.js.map → chunk-DIJ35ZXH.js.map} +0 -0
  75. /package/dist/{chunk-O3QF6QHX.js.map → chunk-Q7ALQS7T.js.map} +0 -0
  76. /package/dist/{reset-FZRYTUFF.js.map → reset-OBM2VJQT.js.map} +0 -0
  77. /package/dist/{setup-WKFBBSLJ.js.map → setup-ZFQDI5QT.js.map} +0 -0
@@ -2,16 +2,19 @@ import {
2
2
  buildIndexConflictListMessage
3
3
  } from "./chunk-ZESHX2BU.js";
4
4
  import {
5
+ fetchRegistryLatestVersion,
5
6
  isNewerVersion
6
- } from "./chunk-FJYRWU2V.js";
7
+ } from "./chunk-LRYWFRPD.js";
7
8
  import {
8
9
  checkHealth,
9
10
  reportHealthSummary
10
- } from "./chunk-O3QF6QHX.js";
11
+ } from "./chunk-Q7ALQS7T.js";
11
12
  import "./chunk-YXNI7W5D.js";
12
13
  import "./chunk-XTLCJKGE.js";
13
- import "./chunk-HGOG5ZLC.js";
14
- import "./chunk-GS3TBFXU.js";
14
+ import "./chunk-UYTOLZT2.js";
15
+ import {
16
+ detectPackageManager
17
+ } from "./chunk-GS3TBFXU.js";
15
18
  import {
16
19
  syncTickets
17
20
  } from "./chunk-HTDMZQKA.js";
@@ -30,26 +33,9 @@ import "./chunk-KIZYVSME.js";
30
33
 
31
34
  // src/commands/check.ts
32
35
  import process from "process";
33
- async function checkLatestVersion(timeout = 3e3) {
34
- try {
35
- const controller = new AbortController();
36
- const timeoutId = setTimeout(() => {
37
- controller.abort();
38
- }, timeout);
39
- const response = await fetch("https://registry.npmjs.org/safeword/latest", {
40
- signal: controller.signal
41
- });
42
- clearTimeout(timeoutId);
43
- if (!response.ok) return void 0;
44
- const data = await response.json();
45
- return data.version ?? void 0;
46
- } catch {
47
- return void 0;
48
- }
49
- }
50
36
  async function reportUpdateStatus(health) {
51
37
  info("\nChecking for updates...");
52
- const latestVersion = await checkLatestVersion();
38
+ const latestVersion = await fetchRegistryLatestVersion();
53
39
  if (!latestVersion) {
54
40
  warn("Couldn't check for updates (offline?)");
55
41
  return;
@@ -63,11 +49,33 @@ async function reportUpdateStatus(health) {
63
49
  success("CLI is up to date");
64
50
  }
65
51
  }
66
- function reportVersionMismatch(health) {
52
+ function isDigit(char) {
53
+ return char >= "0" && char <= "9";
54
+ }
55
+ function isPackageVersionSpecChar(char) {
56
+ return char >= "a" && char <= "z" || char >= "A" && char <= "Z" || isDigit(char) || char === "." || char === "-" || char === "+";
57
+ }
58
+ function isSafePackageVersion(version) {
59
+ if (version.length === 0 || !version.includes(".")) return false;
60
+ for (const char of version) {
61
+ if (!isPackageVersionSpecChar(char)) return false;
62
+ }
63
+ return true;
64
+ }
65
+ function reportVersionMismatch(health, cwd) {
67
66
  if (!health.projectVersion) return;
68
67
  if (isNewerVersion(health.cliVersion, health.projectVersion)) {
69
68
  warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);
70
- info("Consider upgrading the CLI");
69
+ if (!isSafePackageVersion(health.projectVersion)) {
70
+ warn(
71
+ "Project version is not safe to use in a package install command; inspect .safeword/version."
72
+ );
73
+ return;
74
+ }
75
+ const pm = detectPackageManager(cwd);
76
+ const runSafewordUpgrade = pm === "bun" || pm === "yarn" ? `${pm} run safeword upgrade` : `${pm} exec safeword upgrade`;
77
+ info("Update the project-local CLI first:");
78
+ info(`${pm} add -D safeword@${health.projectVersion} && ${runSafewordUpgrade}`);
71
79
  } else if (isNewerVersion(health.projectVersion, health.cliVersion)) {
72
80
  info(`
73
81
  Upgrade available for project config`);
@@ -108,7 +116,7 @@ async function check(options) {
108
116
  } else {
109
117
  await reportUpdateStatus(health);
110
118
  }
111
- reportVersionMismatch(health);
119
+ reportVersionMismatch(health, cwd);
112
120
  const hasIssues = reportHealthSummary(health);
113
121
  if (hasIssues) {
114
122
  process.exit(1);
@@ -117,4 +125,4 @@ async function check(options) {
117
125
  export {
118
126
  check
119
127
  };
120
- //# sourceMappingURL=check-IV6KC65F.js.map
128
+ //# sourceMappingURL=check-6IHMMAS6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["/**\n * Check command - Verify project health and configuration\n *\n * The config-health core lives in ../health.ts (shared with the setup/upgrade\n * self-verify, ticket 3293WH). This command adds the standalone-only\n * surfaces: npm update-check, version display, and ticket-index refresh.\n */\n\nimport process from 'node:process';\n\nimport { checkHealth, type HealthStatus, reportHealthSummary } from '../health.js';\nimport { syncTickets } from '../ticket-sync/index.js';\nimport { detectPackageManager } from '../utils/install.js';\nimport { header, info, keyValue, success, warn } from '../utils/output.js';\nimport { buildIndexConflictListMessage } from '../utils/ticket-index-warnings.js';\nimport { fetchRegistryLatestVersion, isNewerVersion } from '../utils/version.js';\n\ninterface CheckOptions {\n offline?: boolean;\n}\n\n/**\n * Check for CLI updates and report status\n * @param health\n */\nasync function reportUpdateStatus(health: HealthStatus): Promise<void> {\n info('\\nChecking for updates...');\n const latestVersion = await fetchRegistryLatestVersion();\n\n if (!latestVersion) {\n warn(\"Couldn't check for updates (offline?)\");\n return;\n }\n\n health.latestVersion = latestVersion;\n health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);\n\n if (health.updateAvailable) {\n warn(`Update available: v${latestVersion}`);\n info('Run `bunx safeword@latest upgrade` to upgrade');\n } else {\n success('CLI is up to date');\n }\n}\n\nfunction isDigit(char: string): boolean {\n return char >= '0' && char <= '9';\n}\n\nfunction isPackageVersionSpecChar(char: string): boolean {\n return (\n (char >= 'a' && char <= 'z') ||\n (char >= 'A' && char <= 'Z') ||\n isDigit(char) ||\n char === '.' ||\n char === '-' ||\n char === '+'\n );\n}\n\nfunction isSafePackageVersion(version: string): boolean {\n if (version.length === 0 || !version.includes('.')) return false;\n for (const char of version) {\n if (!isPackageVersionSpecChar(char)) return false;\n }\n return true;\n}\n\n/**\n * Compare project version vs CLI version and report\n * @param health\n */\nfunction reportVersionMismatch(health: HealthStatus, cwd: string): void {\n if (!health.projectVersion) return;\n\n if (isNewerVersion(health.cliVersion, health.projectVersion)) {\n warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);\n\n if (!isSafePackageVersion(health.projectVersion)) {\n warn(\n 'Project version is not safe to use in a package install command; inspect .safeword/version.',\n );\n return;\n }\n\n const pm = detectPackageManager(cwd);\n const runSafewordUpgrade =\n pm === 'bun' || pm === 'yarn' ? `${pm} run safeword upgrade` : `${pm} exec safeword upgrade`;\n info('Update the project-local CLI first:');\n info(`${pm} add -D safeword@${health.projectVersion} && ${runSafewordUpgrade}`);\n } else if (isNewerVersion(health.projectVersion, health.cliVersion)) {\n info(`\\nUpgrade available for project config`);\n info(\n `Run \\`safeword upgrade\\` to update from v${health.projectVersion} to v${health.cliVersion}`,\n );\n }\n}\n\n/**\n * Regenerate the ticket discovery index, swallowing any error — index\n * freshness must never block or fail a health check. Reports only when it\n * actually rewrote a file.\n * @param cwd\n */\nfunction regenerateTicketIndex(cwd: string): void {\n try {\n const result = syncTickets(cwd);\n if (result.wrote) {\n info('Regenerated ticket index (INDEX.md / INDEX-completed.md)');\n }\n if (result.indexConflicts.length > 0) {\n warn(buildIndexConflictListMessage(result.indexConflicts));\n }\n } catch (error: unknown) {\n // Best-effort: index freshness must never fail the health check. Surface\n // under DEBUG, then return — the deliberate swallow point.\n if (process.env.DEBUG) {\n console.error('[check] ticket index regen failed:', error);\n }\n return;\n }\n}\n\n/**\n *\n * @param options\n */\nexport async function check(options: CheckOptions): Promise<void> {\n const cwd = process.cwd();\n\n header('Safeword Health Check');\n\n const health = await checkHealth(cwd);\n\n // Not configured\n if (!health.configured) {\n info('Not configured. Run `safeword setup` to initialize.');\n return;\n }\n\n // Keep the ticket discovery index fresh at this checkpoint (best-effort —\n // never fail the health check on index regen). Ticket 1GGD28.\n regenerateTicketIndex(cwd);\n\n // Show versions\n keyValue('Safeword CLI', `v${health.cliVersion}`);\n keyValue('Project config', health.projectVersion ? `v${health.projectVersion}` : 'unknown');\n\n // Check for updates (unless offline)\n if (options.offline) {\n info('\\nSkipped update check (offline mode)');\n } else {\n await reportUpdateStatus(health);\n }\n\n reportVersionMismatch(health, cwd);\n const hasIssues = reportHealthSummary(health);\n\n if (hasIssues) {\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,OAAO,aAAa;AAiBpB,eAAe,mBAAmB,QAAqC;AACrE,OAAK,2BAA2B;AAChC,QAAM,gBAAgB,MAAM,2BAA2B;AAEvD,MAAI,CAAC,eAAe;AAClB,SAAK,uCAAuC;AAC5C;AAAA,EACF;AAEA,SAAO,gBAAgB;AACvB,SAAO,kBAAkB,eAAe,OAAO,YAAY,aAAa;AAExE,MAAI,OAAO,iBAAiB;AAC1B,SAAK,sBAAsB,aAAa,EAAE;AAC1C,SAAK,+CAA+C;AAAA,EACtD,OAAO;AACL,YAAQ,mBAAmB;AAAA,EAC7B;AACF;AAEA,SAAS,QAAQ,MAAuB;AACtC,SAAO,QAAQ,OAAO,QAAQ;AAChC;AAEA,SAAS,yBAAyB,MAAuB;AACvD,SACG,QAAQ,OAAO,QAAQ,OACvB,QAAQ,OAAO,QAAQ,OACxB,QAAQ,IAAI,KACZ,SAAS,OACT,SAAS,OACT,SAAS;AAEb;AAEA,SAAS,qBAAqB,SAA0B;AACtD,MAAI,QAAQ,WAAW,KAAK,CAAC,QAAQ,SAAS,GAAG,EAAG,QAAO;AAC3D,aAAW,QAAQ,SAAS;AAC1B,QAAI,CAAC,yBAAyB,IAAI,EAAG,QAAO;AAAA,EAC9C;AACA,SAAO;AACT;AAMA,SAAS,sBAAsB,QAAsB,KAAmB;AACtE,MAAI,CAAC,OAAO,eAAgB;AAE5B,MAAI,eAAe,OAAO,YAAY,OAAO,cAAc,GAAG;AAC5D,SAAK,oBAAoB,OAAO,cAAc,yBAAyB,OAAO,UAAU,GAAG;AAE3F,QAAI,CAAC,qBAAqB,OAAO,cAAc,GAAG;AAChD;AAAA,QACE;AAAA,MACF;AACA;AAAA,IACF;AAEA,UAAM,KAAK,qBAAqB,GAAG;AACnC,UAAM,qBACJ,OAAO,SAAS,OAAO,SAAS,GAAG,EAAE,0BAA0B,GAAG,EAAE;AACtE,SAAK,qCAAqC;AAC1C,SAAK,GAAG,EAAE,oBAAoB,OAAO,cAAc,OAAO,kBAAkB,EAAE;AAAA,EAChF,WAAW,eAAe,OAAO,gBAAgB,OAAO,UAAU,GAAG;AACnE,SAAK;AAAA,qCAAwC;AAC7C;AAAA,MACE,4CAA4C,OAAO,cAAc,QAAQ,OAAO,UAAU;AAAA,IAC5F;AAAA,EACF;AACF;AAQA,SAAS,sBAAsB,KAAmB;AAChD,MAAI;AACF,UAAM,SAAS,YAAY,GAAG;AAC9B,QAAI,OAAO,OAAO;AAChB,WAAK,0DAA0D;AAAA,IACjE;AACA,QAAI,OAAO,eAAe,SAAS,GAAG;AACpC,WAAK,8BAA8B,OAAO,cAAc,CAAC;AAAA,IAC3D;AAAA,EACF,SAAS,OAAgB;AAGvB,QAAI,QAAQ,IAAI,OAAO;AACrB,cAAQ,MAAM,sCAAsC,KAAK;AAAA,IAC3D;AACA;AAAA,EACF;AACF;AAMA,eAAsB,MAAM,SAAsC;AAChE,QAAM,MAAM,QAAQ,IAAI;AAExB,SAAO,uBAAuB;AAE9B,QAAM,SAAS,MAAM,YAAY,GAAG;AAGpC,MAAI,CAAC,OAAO,YAAY;AACtB,SAAK,qDAAqD;AAC1D;AAAA,EACF;AAIA,wBAAsB,GAAG;AAGzB,WAAS,gBAAgB,IAAI,OAAO,UAAU,EAAE;AAChD,WAAS,kBAAkB,OAAO,iBAAiB,IAAI,OAAO,cAAc,KAAK,SAAS;AAG1F,MAAI,QAAQ,SAAS;AACnB,SAAK,uCAAuC;AAAA,EAC9C,OAAO;AACL,UAAM,mBAAmB,MAAM;AAAA,EACjC;AAEA,wBAAsB,QAAQ,GAAG;AACjC,QAAM,YAAY,oBAAoB,MAAM;AAE5C,MAAI,WAAW;AACb,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  addInstalledPack,
4
4
  isGitRepo,
5
5
  isPackInstalled
6
- } from "./chunk-HGOG5ZLC.js";
6
+ } from "./chunk-UYTOLZT2.js";
7
7
  import {
8
8
  SAFEWORD_PEER_DEPENDENCIES
9
9
  } from "./chunk-HSC7TELY.js";
@@ -392,4 +392,4 @@ export {
392
392
  getEslintPeerMismatchWarning,
393
393
  maybeAutoPatchOrNudge
394
394
  };
395
- //# sourceMappingURL=chunk-D5H7VBXQ.js.map
395
+ //# sourceMappingURL=chunk-DIJ35ZXH.js.map
@@ -0,0 +1,46 @@
1
+ // src/utils/version.ts
2
+ function compareVersions(a, b) {
3
+ const aParts = a.split(".").map(Number);
4
+ const bParts = b.split(".").map(Number);
5
+ for (let i = 0; i < 3; i++) {
6
+ const aValue = aParts[i] ?? 0;
7
+ const bValue = bParts[i] ?? 0;
8
+ if (aValue < bValue) return -1;
9
+ if (aValue > bValue) return 1;
10
+ }
11
+ return 0;
12
+ }
13
+ function isNewerVersion(current, latest) {
14
+ return compareVersions(current, latest) === -1;
15
+ }
16
+ var REGISTRY_TIMEOUT_MS = 3e3;
17
+ var VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
18
+ function isComparableVersion(value) {
19
+ return typeof value === "string" && VERSION_PATTERN.test(value);
20
+ }
21
+ async function fetchRegistryLatestVersion(timeout = REGISTRY_TIMEOUT_MS) {
22
+ const controller = new AbortController();
23
+ const timeoutId = setTimeout(() => {
24
+ controller.abort();
25
+ }, timeout);
26
+ try {
27
+ const response = await fetch("https://registry.npmjs.org/safeword/latest", {
28
+ signal: controller.signal
29
+ });
30
+ if (!response.ok) return void 0;
31
+ const data = await response.json();
32
+ return isComparableVersion(data.version) ? data.version : void 0;
33
+ } catch {
34
+ return void 0;
35
+ } finally {
36
+ clearTimeout(timeoutId);
37
+ }
38
+ }
39
+
40
+ export {
41
+ compareVersions,
42
+ isNewerVersion,
43
+ isComparableVersion,
44
+ fetchRegistryLatestVersion
45
+ };
46
+ //# sourceMappingURL=chunk-LRYWFRPD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/version.ts"],"sourcesContent":["/**\n * Version comparison utilities\n */\n\n/**\n * Compare two semver versions\n * @param a\n * @param b\n * @returns -1 if a < b, 0 if a == b, 1 if a > b\n */\nexport function compareVersions(a: string, b: string): -1 | 0 | 1 {\n const aParts = a.split('.').map(Number);\n const bParts = b.split('.').map(Number);\n\n for (let i = 0; i < 3; i++) {\n const aValue = aParts[i] ?? 0;\n const bValue = bParts[i] ?? 0;\n if (aValue < bValue) return -1;\n if (aValue > bValue) return 1;\n }\n\n return 0;\n}\n\n/**\n * Check if latest version is newer than current\n * @param current\n * @param latest\n */\nexport function isNewerVersion(current: string, latest: string): boolean {\n return compareVersions(current, latest) === -1;\n}\n\nconst REGISTRY_TIMEOUT_MS = 3000;\nconst VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\n\n/**\n * Type guard for a comparable semver string (e.g. `\"1.2.3\"`). Use before\n * passing a registry- or cache-sourced value into {@link compareVersions},\n * which silently produces `NaN` comparisons on malformed input.\n * @param value\n */\nexport function isComparableVersion(value: unknown): value is string {\n return typeof value === 'string' && VERSION_PATTERN.test(value);\n}\n\n/**\n * Fetch the latest published safeword version from the npm registry.\n * Returns undefined on network error, timeout, non-OK response, or an\n * unparseable/invalid version — callers treat undefined as \"unknown\".\n * @param timeout milliseconds before the request is aborted\n */\nexport async function fetchRegistryLatestVersion(\n timeout = REGISTRY_TIMEOUT_MS,\n): Promise<string | undefined> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort();\n }, timeout);\n\n try {\n const response = await fetch('https://registry.npmjs.org/safeword/latest', {\n signal: controller.signal,\n });\n if (!response.ok) return undefined;\n\n const data = (await response.json()) as { version?: unknown };\n return isComparableVersion(data.version) ? data.version : undefined;\n } catch {\n return undefined;\n } finally {\n clearTimeout(timeoutId);\n }\n}\n"],"mappings":";AAUO,SAAS,gBAAgB,GAAW,GAAuB;AAChE,QAAM,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM;AACtC,QAAM,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM;AAEtC,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAM,SAAS,OAAO,CAAC,KAAK;AAC5B,UAAM,SAAS,OAAO,CAAC,KAAK;AAC5B,QAAI,SAAS,OAAQ,QAAO;AAC5B,QAAI,SAAS,OAAQ,QAAO;AAAA,EAC9B;AAEA,SAAO;AACT;AAOO,SAAS,eAAe,SAAiB,QAAyB;AACvE,SAAO,gBAAgB,SAAS,MAAM,MAAM;AAC9C;AAEA,IAAM,sBAAsB;AAC5B,IAAM,kBAAkB;AAQjB,SAAS,oBAAoB,OAAiC;AACnE,SAAO,OAAO,UAAU,YAAY,gBAAgB,KAAK,KAAK;AAChE;AAQA,eAAsB,2BACpB,UAAU,qBACmB;AAC7B,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,YAAY,WAAW,MAAM;AACjC,eAAW,MAAM;AAAA,EACnB,GAAG,OAAO;AAEV,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,8CAA8C;AAAA,MACzE,QAAQ,WAAW;AAAA,IACrB,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,oBAAoB,KAAK,OAAO,IAAI,KAAK,UAAU;AAAA,EAC5D,QAAQ;AACN,WAAO;AAAA,EACT,UAAE;AACA,iBAAa,SAAS;AAAA,EACxB;AACF;","names":[]}
@@ -14,7 +14,7 @@ import {
14
14
  createProjectContext,
15
15
  getMissingPacks,
16
16
  reconcile
17
- } from "./chunk-HGOG5ZLC.js";
17
+ } from "./chunk-UYTOLZT2.js";
18
18
  import {
19
19
  findDanglingDependencies,
20
20
  findTicketsInCycles,
@@ -723,4 +723,4 @@ export {
723
723
  checkHealth,
724
724
  reportHealthSummary
725
725
  };
726
- //# sourceMappingURL=chunk-O3QF6QHX.js.map
726
+ //# sourceMappingURL=chunk-Q7ALQS7T.js.map
@@ -1038,21 +1038,25 @@ function executeTextPatch(cwd, path, definition) {
1038
1038
  throw new Error(`rerender text patch ${path} must use operation: 'append'`);
1039
1039
  }
1040
1040
  const fullPath = nodePath7.join(cwd, path);
1041
- let content = readFileSafe(fullPath) ?? "";
1041
+ const original = readFileSafe(fullPath) ?? "";
1042
+ const content = definition.supersedes === void 0 ? original : original.replace(definition.supersedes, "");
1042
1043
  if (content.includes(definition.marker)) {
1043
- if (definition.rerender && !content.includes(definition.content)) {
1044
- const stripped = stripRerenderBlock(content, definition);
1045
- writeFile(fullPath, stripped + definition.content);
1046
- return;
1047
- }
1048
- if (content.includes("\n\n---#")) {
1049
- const healed = content.replaceAll("\n\n---#", "\n\n---\n\n#");
1050
- writeFile(fullPath, healed);
1051
- }
1044
+ healAlreadyPatchedFile(fullPath, original, content, definition);
1052
1045
  return;
1053
1046
  }
1054
- content = definition.operation === "prepend" ? definition.content + content : content + definition.content;
1055
- writeFile(fullPath, content);
1047
+ const patched = definition.operation === "prepend" ? definition.content + content : content + definition.content;
1048
+ writeFile(fullPath, patched);
1049
+ }
1050
+ function healAlreadyPatchedFile(fullPath, original, content, definition) {
1051
+ if (definition.rerender && !content.includes(definition.content)) {
1052
+ writeFile(fullPath, stripRerenderBlock(content, definition) + definition.content);
1053
+ return;
1054
+ }
1055
+ let healed = content;
1056
+ if (healed.includes("\n\n---#")) {
1057
+ healed = healed.replaceAll("\n\n---#", "\n\n---\n\n#");
1058
+ }
1059
+ if (healed !== original) writeFile(fullPath, healed);
1056
1060
  }
1057
1061
  function computeUnpatchedContent(content, definition) {
1058
1062
  let unpatched = removeExactTextPatchContent(content, definition);
@@ -2149,9 +2153,12 @@ ${prettier.configEntry}
2149
2153
  `;
2150
2154
  }
2151
2155
  var CURSOR_HOOKS = {
2152
- // Observational: injects standing context. Fail-open a failed inject must not
2153
- // block the session from starting.
2154
- sessionStart: [{ command: "bun ./.safeword/hooks/session-safeword-context.ts --agent=cursor" }],
2156
+ // Observational: injects standing context and checks for auto-upgrades.
2157
+ // Fail-open — neither hook may block the session from starting.
2158
+ sessionStart: [
2159
+ { command: "bun ./.safeword/hooks/session-safeword-context.ts --agent=cursor" },
2160
+ { command: "bun ./.safeword/hooks/session-cursor-auto-upgrade.ts" }
2161
+ ],
2155
2162
  // NOTE (F2TKR3): there is deliberately NO beforeSubmitPrompt gate. That hook
2156
2163
  // fires at prompt-send time, where Cursor exposes only the prompt text — no tool
2157
2164
  // name or file path — so it cannot tell "create test-definitions.md" from "write
@@ -2249,13 +2256,22 @@ var SETTINGS_HOOKS = {
2249
2256
  "Bash",
2250
2257
  "Bash(git commit*)",
2251
2258
  `bun ${HOOKS_DIR}/pre-tool-architecture-stage.ts`
2252
- )
2259
+ ),
2260
+ // Warn (never block) before a checkout/switch to a branch behind its upstream,
2261
+ // so "catch up to main" doesn't silently serve stale content (#366). `if`
2262
+ // scopes the spawn to checkout/switch; the hook re-parses the target.
2263
+ matchedHookWithIf("Bash", "Bash(git checkout*)", `bun ${HOOKS_DIR}/pre-tool-stale-main.ts`),
2264
+ matchedHookWithIf("Bash", "Bash(git switch*)", `bun ${HOOKS_DIR}/pre-tool-stale-main.ts`)
2253
2265
  ],
2254
2266
  PostToolUse: [
2255
2267
  matchedHook(EDIT_TOOLS, `bun ${HOOKS_DIR}/post-tool-lint.ts`),
2256
2268
  matchedHook(`${EDIT_TOOLS}|Bash`, `bun ${HOOKS_DIR}/post-tool-quality.ts`),
2257
2269
  matchedHook(EDIT_TOOLS, `bun ${HOOKS_DIR}/post-tool-bypass-warn.ts`),
2258
- matchedHook(EDIT_TOOLS, `bun ${HOOKS_DIR}/post-tool-sync-learnings.ts`)
2270
+ matchedHook(EDIT_TOOLS, `bun ${HOOKS_DIR}/post-tool-sync-learnings.ts`),
2271
+ // Stamp the dependency fingerprint after a successful install so the
2272
+ // recommended recovery command clears the readiness block (#380). Fast-exits
2273
+ // on non-install Bash commands.
2274
+ matchedHook("Bash", `bun ${HOOKS_DIR}/post-tool-dependency-readiness.ts`)
2259
2275
  ],
2260
2276
  SessionEnd: [hook(`bun ${HOOKS_DIR}/session-cleanup-quality.ts`)]
2261
2277
  };
@@ -2644,11 +2660,13 @@ var CLAUDE_MD_IMPORT_BLOCK = `@./.safeword/SAFEWORD.md
2644
2660
  function isHookEntry(h) {
2645
2661
  return typeof h === "object" && h !== null && "hooks" in h && Array.isArray(h.hooks);
2646
2662
  }
2663
+ function isCursorHookEntry(h) {
2664
+ return typeof h === "object" && h !== null && typeof h.command === "string";
2665
+ }
2647
2666
  function isSafewordHook(h) {
2667
+ if (isCursorHookEntry(h)) return h.command.includes(".safeword");
2648
2668
  if (!isHookEntry(h)) return false;
2649
- return h.hooks.some(
2650
- (command) => typeof command.command === "string" && command.command.includes(".safeword")
2651
- );
2669
+ return h.hooks.some((command) => command.command.includes(".safeword"));
2652
2670
  }
2653
2671
  function filterOutSafewordHooks(hooks) {
2654
2672
  return hooks.filter((h) => !isSafewordHook(h));
@@ -2687,6 +2705,16 @@ var CODEX_SESSION_START_HOOK_PATCH = `
2687
2705
  [[hooks.SessionStart]]
2688
2706
  matcher = ""
2689
2707
 
2708
+ [[hooks.SessionStart.hooks]]
2709
+ type = "command"
2710
+ command = 'bun "$(git rev-parse --show-toplevel)/.safeword/hooks/session-codex-start.ts"'
2711
+ timeout = 120
2712
+ statusMessage = "Checking safeword updates and loading standing instructions"
2713
+ `;
2714
+ var CODEX_LEGACY_CONTEXT_SESSION_START_HOOK_PATCH = `
2715
+ [[hooks.SessionStart]]
2716
+ matcher = ""
2717
+
2690
2718
  [[hooks.SessionStart.hooks]]
2691
2719
  type = "command"
2692
2720
  command = 'bun "$(git rev-parse --show-toplevel)/.safeword/hooks/session-safeword-context.ts" --agent=codex'
@@ -2758,6 +2786,7 @@ var CODEX_SKILL_OWNED_FILES = Object.fromEntries(
2758
2786
  );
2759
2787
  var NAMESPACE_TRANSIENT_BASENAMES = [
2760
2788
  "quality-state*.json",
2789
+ "cursor-run-identity.json",
2761
2790
  "failure-counts.json",
2762
2791
  "skill-invocations.log",
2763
2792
  "re-entry.md",
@@ -2970,7 +2999,14 @@ var SAFEWORD_SCHEMA = {
2970
2999
  },
2971
3000
  // Hooks shared library - TypeScript with Bun runtime
2972
3001
  ".safeword/hooks/lib/active-ticket.ts": { template: "hooks/lib/active-ticket.ts" },
3002
+ ".safeword/hooks/lib/architecture-staged-scope.ts": {
3003
+ template: "hooks/lib/architecture-staged-scope.ts"
3004
+ },
3005
+ ".safeword/hooks/lib/branch-staleness.ts": { template: "hooks/lib/branch-staleness.ts" },
2973
3006
  ".safeword/hooks/lib/blocked-on-gate.ts": { template: "hooks/lib/blocked-on-gate.ts" },
3007
+ ".safeword/hooks/lib/cursor-run-identity.ts": {
3008
+ template: "hooks/lib/cursor-run-identity.ts"
3009
+ },
2974
3010
  ".safeword/hooks/lib/git-operation.ts": { template: "hooks/lib/git-operation.ts" },
2975
3011
  ".safeword/hooks/lib/re-entry.ts": { template: "hooks/lib/re-entry.ts" },
2976
3012
  ".safeword/hooks/lib/hierarchy.ts": { template: "hooks/lib/hierarchy.ts" },
@@ -2999,9 +3035,13 @@ var SAFEWORD_SCHEMA = {
2999
3035
  },
3000
3036
  ".safeword/hooks/lib/review-trigger.ts": { template: "hooks/lib/review-trigger.ts" },
3001
3037
  ".safeword/hooks/lib/dogfood.ts": { template: "hooks/lib/dogfood.ts" },
3038
+ ".safeword/hooks/lib/ledger-git.ts": { template: "hooks/lib/ledger-git.ts" },
3002
3039
  ".safeword/hooks/lib/ledger-validation.ts": { template: "hooks/lib/ledger-validation.ts" },
3003
3040
  ".safeword/hooks/lib/scenario-format.ts": { template: "hooks/lib/scenario-format.ts" },
3004
3041
  ".safeword/hooks/lib/test-runner.ts": { template: "hooks/lib/test-runner.ts" },
3042
+ ".safeword/hooks/lib/auto-upgrade.ts": { template: "hooks/lib/auto-upgrade.ts" },
3043
+ ".safeword/hooks/lib/auto-upgrade-lock.ts": { template: "hooks/lib/auto-upgrade-lock.ts" },
3044
+ ".safeword/hooks/lib/safeword-context.ts": { template: "hooks/lib/safeword-context.ts" },
3005
3045
  ".safeword/hooks/lib/update-cache.ts": { template: "hooks/lib/update-cache.ts" },
3006
3046
  ".safeword/hooks/lib/version.ts": { template: "hooks/lib/version.ts" },
3007
3047
  ".safeword/hooks/lib/learning-verification-stamps.ts": {
@@ -3017,6 +3057,12 @@ var SAFEWORD_SCHEMA = {
3017
3057
  ".safeword/hooks/session-safeword-context.ts": {
3018
3058
  template: "hooks/session-safeword-context.ts"
3019
3059
  },
3060
+ ".safeword/hooks/session-codex-start.ts": {
3061
+ template: "hooks/session-codex-start.ts"
3062
+ },
3063
+ ".safeword/hooks/session-cursor-auto-upgrade.ts": {
3064
+ template: "hooks/session-cursor-auto-upgrade.ts"
3065
+ },
3020
3066
  ".safeword/hooks/session-dependency-readiness.ts": {
3021
3067
  template: "hooks/session-dependency-readiness.ts"
3022
3068
  },
@@ -3056,6 +3102,9 @@ var SAFEWORD_SCHEMA = {
3056
3102
  ".safeword/hooks/pre-tool-architecture-stage.ts": {
3057
3103
  template: "hooks/pre-tool-architecture-stage.ts"
3058
3104
  },
3105
+ ".safeword/hooks/pre-tool-stale-main.ts": {
3106
+ template: "hooks/pre-tool-stale-main.ts"
3107
+ },
3059
3108
  ".safeword/hooks/codex/pre-tool-quality.ts": {
3060
3109
  template: "hooks/codex/pre-tool-quality.ts"
3061
3110
  },
@@ -3071,6 +3120,9 @@ var SAFEWORD_SCHEMA = {
3071
3120
  ".safeword/hooks/pre-tool-dependency-readiness.ts": {
3072
3121
  template: "hooks/pre-tool-dependency-readiness.ts"
3073
3122
  },
3123
+ ".safeword/hooks/post-tool-dependency-readiness.ts": {
3124
+ template: "hooks/post-tool-dependency-readiness.ts"
3125
+ },
3074
3126
  ".safeword/hooks/pre-tool-git-bare-fix.sh": {
3075
3127
  template: "hooks/pre-tool-git-bare-fix.sh"
3076
3128
  },
@@ -3113,12 +3165,18 @@ var SAFEWORD_SCHEMA = {
3113
3165
  ".safeword/guides/llm-writing-guide.md": {
3114
3166
  template: "guides/llm-writing-guide.md"
3115
3167
  },
3168
+ ".safeword/guides/llm-evals-guide.md": {
3169
+ template: "guides/llm-evals-guide.md"
3170
+ },
3116
3171
  ".safeword/guides/planning-guide.md": {
3117
3172
  template: "guides/planning-guide.md"
3118
3173
  },
3119
3174
  ".safeword/guides/testing-guide.md": {
3120
3175
  template: "guides/testing-guide.md"
3121
3176
  },
3177
+ ".safeword/guides/verification-lanes-guide.md": {
3178
+ template: "guides/verification-lanes-guide.md"
3179
+ },
3122
3180
  ".safeword/guides/zombie-process-cleanup.md": {
3123
3181
  template: "guides/zombie-process-cleanup.md"
3124
3182
  },
@@ -3419,23 +3477,25 @@ var SAFEWORD_SCHEMA = {
3419
3477
  ],
3420
3478
  removeFileIfEmpty: true,
3421
3479
  merge: (existing) => {
3422
- const hooks = existing.hooks ?? {};
3480
+ const existingHooks = existing.hooks ?? {};
3481
+ const hooks = { ...existingHooks };
3482
+ for (const [event, newHooks] of Object.entries(CURSOR_HOOKS)) {
3483
+ const eventHooks = hooks[event] ?? [];
3484
+ const nonSafewordHooks = filterOutSafewordHooks(eventHooks);
3485
+ hooks[event] = [...nonSafewordHooks, ...newHooks];
3486
+ }
3423
3487
  return {
3424
3488
  ...existing,
3425
3489
  version: 1,
3426
3490
  // Required by Cursor
3427
- hooks: {
3428
- ...hooks,
3429
- ...CURSOR_HOOKS
3430
- }
3491
+ hooks
3431
3492
  };
3432
3493
  },
3433
3494
  unmerge: (existing) => {
3434
3495
  const result = { ...existing };
3435
3496
  const existingHooks = existing.hooks ?? {};
3436
- const safewordHookNames = new Set(Object.keys(CURSOR_HOOKS));
3437
3497
  const hooks = Object.fromEntries(
3438
- Object.entries(existingHooks).filter(([name]) => !safewordHookNames.has(name))
3498
+ Object.entries(existingHooks).map(([name, eventHooks]) => [name, filterOutSafewordHooks(eventHooks)]).filter(([, eventHooks]) => eventHooks.length > 0)
3439
3499
  );
3440
3500
  if (!assignOrPrune(result, "hooks", hooks)) {
3441
3501
  delete result.version;
@@ -3494,9 +3554,35 @@ ${managedPrettierPaths(ctx).join("\n")}
3494
3554
  "# Safeword Codex project configuration.",
3495
3555
  ".safeword/hooks/codex/pre-tool-quality.ts"
3496
3556
  ],
3497
- unpatchContent: [CODEX_SESSION_START_HOOK_PATCH, CODEX_PRE_TOOL_QUALITY_HOOK_PATCH],
3557
+ unpatchContent: [
3558
+ CODEX_SESSION_START_HOOK_PATCH,
3559
+ CODEX_LEGACY_CONTEXT_SESSION_START_HOOK_PATCH,
3560
+ CODEX_PRE_TOOL_QUALITY_HOOK_PATCH
3561
+ ],
3498
3562
  removeFileIfContentEquals: [CODEX_CONFIG_SCAFFOLD_WITHOUT_HOOKS]
3499
3563
  },
3564
+ // Migrate existing installs (auto-upgrade-codex follow-up to #433): swap the
3565
+ // legacy context-only SessionStart hook for the auto-upgrade dispatcher.
3566
+ // Codex runs same-event hooks concurrently with no ordering, so this must
3567
+ // REPLACE the legacy hook (appending a second SessionStart hook would
3568
+ // double-emit context) — hence `supersedes`. managedFiles is
3569
+ // create-if-missing, so a fresh install gets the dispatcher from the
3570
+ // template while every EXISTING config is skipped by managedFiles and
3571
+ // migrated here. Idempotent: skips when the dispatcher marker is already
3572
+ // present, and the strip no-ops when the legacy block is absent. Guarded to
3573
+ // safeword scaffolds; a user-modified legacy block won't byte-match and is
3574
+ // preserved. Uninstall cleanup is owned by the primary patch's
3575
+ // unpatchContent above.
3576
+ {
3577
+ operation: "append",
3578
+ content: CODEX_SESSION_START_HOOK_PATCH,
3579
+ marker: ".safeword/hooks/session-codex-start.ts",
3580
+ supersedes: CODEX_LEGACY_CONTEXT_SESSION_START_HOOK_PATCH,
3581
+ applyWhenContentIncludes: [
3582
+ "# Safeword Codex project configuration.",
3583
+ ".safeword/hooks/codex/pre-tool-quality.ts"
3584
+ ]
3585
+ },
3500
3586
  // MCP-server retrofit (#269): add-if-missing parity with .mcp.json /
3501
3587
  // .cursor/mcp.json. Marker is the context7 table header, so an existing
3502
3588
  // (safeword- or user-authored) [mcp_servers.context7] suppresses the
@@ -3846,4 +3932,4 @@ export {
3846
3932
  untrackIgnoredFiles,
3847
3933
  createProjectContext
3848
3934
  };
3849
- //# sourceMappingURL=chunk-HGOG5ZLC.js.map
3935
+ //# sourceMappingURL=chunk-UYTOLZT2.js.map