@tenonhq/dovetail-core 0.0.93 → 0.0.95

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/dist/appUtils.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = exports.normalizeManifestKeys = void 0;
39
+ exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = exports.normalizeManifestKeys = exports.toSafeFolderName = void 0;
40
40
  const path_1 = __importDefault(require("path"));
41
41
  const fs_1 = __importDefault(require("fs"));
42
42
  const progress_1 = __importDefault(require("progress"));
@@ -171,9 +171,29 @@ const processTablesInManifest = async (tables, forceWrite, sourcePath, onRecordP
171
171
  });
172
172
  };
173
173
  /**
174
- * Re-keys manifest records from sys_id to record.name (display value).
174
+ * Returns a filesystem-safe folder name for a record.
175
+ *
176
+ * Dovetail names each record's on-disk folder after the record's display name.
177
+ * Windows/NTFS forbids these characters in a path component — `< > : " | ? * \ /`
178
+ * — and forbids a trailing space or trailing dot. A folder whose name violates
179
+ * this cannot be checked out on Windows (`git clone` aborts with "invalid path"),
180
+ * even though macOS/Linux accept it. When the display name is unsafe we fall back
181
+ * to the record's sys_id, which is hex and safe on every OS. The true display name
182
+ * is unaffected here and is still carried by the server-provided metaData.json.
183
+ */
184
+ const toSafeFolderName = (record) => {
185
+ var name = record.name || "";
186
+ var isUnsafe = /[<>:"|?*\\/]/.test(name) || /[ .]$/.test(name) || name === "";
187
+ return isUnsafe ? record.sys_id : name;
188
+ };
189
+ exports.toSafeFolderName = toSafeFolderName;
190
+ /**
191
+ * Re-keys manifest records from sys_id to a filesystem-safe folder name.
175
192
  * Some ServiceNow tables return records keyed by sys_id instead of display name.
176
- * This ensures consistent naming for directories and manifest lookups.
193
+ * This ensures consistent naming for directories and manifest lookups, and keeps
194
+ * the on-disk folder name (which every writer derives from `record.name`) equal to
195
+ * the manifest key that `dove push` looks records up by — so the key ≡ folder
196
+ * invariant holds even when an unsafe display name forces a sys_id fallback.
177
197
  */
178
198
  const normalizeManifestKeys = (manifest) => {
179
199
  var tables = manifest.tables || {};
@@ -186,11 +206,15 @@ const normalizeManifestKeys = (manifest) => {
186
206
  for (var j = 0; j < recordKeys.length; j++) {
187
207
  var key = recordKeys[j];
188
208
  var record = records[key];
189
- var displayKey = record.name || key;
190
- // Handle duplicate display names by appending sys_id suffix
209
+ var displayKey = (0, exports.toSafeFolderName)(record);
210
+ // Handle duplicate folder names by appending sys_id suffix
191
211
  if (normalized[displayKey]) {
192
212
  displayKey = displayKey + " (" + record.sys_id.substring(0, 8) + ")";
193
213
  }
214
+ // Keep record.name === manifest key so all writers (which build the folder
215
+ // path from record.name) and push (which looks up by folder name) agree.
216
+ // The real display name lives in the server-provided metaData.json snapshot.
217
+ record.name = displayKey;
194
218
  normalized[displayKey] = record;
195
219
  }
196
220
  tables[tableName].records = normalized;
@@ -199,6 +223,9 @@ const normalizeManifestKeys = (manifest) => {
199
223
  };
200
224
  exports.normalizeManifestKeys = normalizeManifestKeys;
201
225
  const processManifest = async (manifest, forceWrite = false, sourcePath) => {
226
+ // Idempotent: guarantees filesystem-safe folder names + key ≡ record.name on
227
+ // every write path, including downloadCommand which does not normalize upstream.
228
+ (0, exports.normalizeManifestKeys)(manifest);
202
229
  const tableCount = Object.keys(manifest.tables).length;
203
230
  FileLogger_1.fileLogger.debug("Processing manifest: " + (manifest.scope || "legacy") + " (" + tableCount + " tables)");
204
231
  var recordCount = countRecordsInTables(manifest.tables);
package/dist/commander.js CHANGED
@@ -334,10 +334,10 @@ async function initCommands() {
334
334
  }, async (args) => {
335
335
  await (0, dashboardCommand_1.dashboardCommand)(args);
336
336
  })
337
- .command("schema <subcommand>", "Manage ServiceNow table schemas (subcommands: pull)", (cmdArgs) => {
337
+ .command("schema <subcommand>", "Manage ServiceNow table schemas (subcommands: pull, diff, snapshots)", (cmdArgs) => {
338
338
  cmdArgs.positional("subcommand", {
339
339
  describe: "Schema subcommand to run",
340
- choices: ["pull"],
340
+ choices: ["pull", "diff", "snapshots"],
341
341
  });
342
342
  cmdArgs.options({
343
343
  ...sharedOptions,
@@ -349,7 +349,24 @@ async function initCommands() {
349
349
  scope: {
350
350
  alias: "s",
351
351
  type: "string",
352
- describe: "Pull schema for a single scope (default: all scopes from dove.config.js)",
352
+ describe: "Single scope (default: all scopes from dove.config.js)",
353
+ },
354
+ snapshot: {
355
+ type: "string",
356
+ describe: "pull: also save an immutable snapshot with this label (timestamp-only if no label)",
357
+ },
358
+ from: {
359
+ type: "string",
360
+ describe: "diff: baseline ref — a snapshot label, snapshot dir name, or directory path (default: newest snapshot)",
361
+ },
362
+ to: {
363
+ type: "string",
364
+ describe: 'diff: comparison ref — "live" for a fresh pull, or a snapshot ref (default: live)',
365
+ },
366
+ format: {
367
+ type: "string",
368
+ choices: ["text", "json"],
369
+ describe: "diff: output format (default: text)",
353
370
  },
354
371
  });
355
372
  return cmdArgs;
@@ -357,6 +374,12 @@ async function initCommands() {
357
374
  if (args.subcommand === "pull") {
358
375
  await (0, schemaCommand_1.schemaPullCommand)(args);
359
376
  }
377
+ else if (args.subcommand === "diff") {
378
+ await (0, schemaCommand_1.schemaDiffCommand)(args);
379
+ }
380
+ else if (args.subcommand === "snapshots") {
381
+ await (0, schemaCommand_1.schemaSnapshotsCommand)(args);
382
+ }
360
383
  })
361
384
  .command("init-claude", "Install Dovetail Claude Code skills to .claude/commands/", (cmdArgs) => {
362
385
  cmdArgs.options({
@@ -37,32 +37,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.schemaPullCommand = schemaPullCommand;
40
+ exports.schemaDiffCommand = schemaDiffCommand;
41
+ exports.schemaSnapshotsCommand = schemaSnapshotsCommand;
40
42
  const dovetail_schema_1 = require("@tenonhq/dovetail-schema");
41
43
  const Logger_1 = require("./Logger");
42
44
  const ConfigManager = __importStar(require("./config"));
43
45
  const path_1 = __importDefault(require("path"));
44
- async function schemaPullCommand(args) {
46
+ const os_1 = __importDefault(require("os"));
47
+ const fs_1 = require("fs");
48
+ function requireCreds() {
45
49
  const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "" } = process.env;
46
50
  if (!SN_USER || !SN_PASSWORD || !SN_INSTANCE) {
47
51
  throw new Error("Missing ServiceNow credentials. Ensure SN_INSTANCE, SN_USER, and SN_PASSWORD are set in your .env file or environment.");
48
52
  }
53
+ return { SN_USER, SN_PASSWORD, SN_INSTANCE };
54
+ }
55
+ function normalizeInstance(instance) {
56
+ return instance.replace(/^https?:\/\//, "").replace(/\/$/, "");
57
+ }
58
+ function resolveScopes(args) {
49
59
  const config = ConfigManager.getConfig();
50
60
  const configScopes = config.scopes ? Object.keys(config.scopes) : [];
51
61
  if (configScopes.length === 0) {
52
62
  throw new Error("No scopes configured in dove.config.js. Add scopes to the 'scopes' object in your configuration.");
53
63
  }
54
- // If --scope flag is provided, filter to that single scope
55
- let scopes = configScopes;
56
64
  if (args.scope) {
57
65
  if (!configScopes.includes(args.scope)) {
58
66
  throw new Error(`Scope "${args.scope}" is not configured in dove.config.js. Available scopes: ${configScopes.join(", ")}`);
59
67
  }
60
- scopes = [args.scope];
68
+ return [args.scope];
61
69
  }
70
+ return configScopes;
71
+ }
72
+ function outputDirFor(args) {
62
73
  const rootDir = ConfigManager.getRootDir();
63
- const outputDir = args.output
64
- ? path_1.default.resolve(args.output)
65
- : path_1.default.join(rootDir, "schema");
74
+ return args.output ? path_1.default.resolve(args.output) : path_1.default.join(rootDir, "schema");
75
+ }
76
+ async function schemaPullCommand(args) {
77
+ const { SN_USER, SN_PASSWORD, SN_INSTANCE } = requireCreds();
78
+ const scopes = resolveScopes(args);
79
+ const outputDir = outputDirFor(args);
66
80
  try {
67
81
  const index = await (0, dovetail_schema_1.pullSchema)({
68
82
  instance: SN_INSTANCE,
@@ -72,6 +86,18 @@ async function schemaPullCommand(args) {
72
86
  scopes,
73
87
  });
74
88
  Logger_1.logger.success(`Schema pull complete! ${index.total_tables} tables across ${index.applications.length} applications written to ${outputDir}`);
89
+ if (args.snapshot !== undefined && args.snapshot !== false) {
90
+ const label = typeof args.snapshot === "string" && args.snapshot.length > 0
91
+ ? args.snapshot
92
+ : null;
93
+ const info = await (0, dovetail_schema_1.writeSnapshot)({
94
+ outputDir,
95
+ index,
96
+ label,
97
+ now: new Date().toISOString(),
98
+ });
99
+ Logger_1.logger.success("Snapshot saved" + (label ? ` (${label})` : "") + ": " + info.dir);
100
+ }
75
101
  }
76
102
  catch (e) {
77
103
  let message;
@@ -83,3 +109,71 @@ async function schemaPullCommand(args) {
83
109
  throw e;
84
110
  }
85
111
  }
112
+ // Resolve a diff ref to a directory of schema files. `live` triggers a fresh
113
+ // pull into a temp dir; everything else resolves to a snapshot/baseline dir.
114
+ async function resolveRefDir(options) {
115
+ const { ref, outputDir, instance, scopes, creds } = options;
116
+ if (ref === "live") {
117
+ const tmpDir = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "dove-schema-live-"));
118
+ await (0, dovetail_schema_1.pullSchema)({
119
+ instance: creds.SN_INSTANCE,
120
+ username: creds.SN_USER,
121
+ password: creds.SN_PASSWORD,
122
+ outputDir: tmpDir,
123
+ scopes,
124
+ });
125
+ return tmpDir;
126
+ }
127
+ return (0, dovetail_schema_1.resolveSnapshotDir)({ ref, outputDir, instance });
128
+ }
129
+ async function schemaDiffCommand(args) {
130
+ const creds = requireCreds();
131
+ const instance = normalizeInstance(creds.SN_INSTANCE);
132
+ const outputDir = outputDirFor(args);
133
+ const scopes = resolveScopes(args);
134
+ const scope = args.scope || null;
135
+ const format = args.format === "json" ? "json" : "text";
136
+ // Default `from` = newest snapshot for this instance; default `to` = live.
137
+ let fromRef = args.from;
138
+ if (!fromRef) {
139
+ const snapshots = await (0, dovetail_schema_1.listSnapshots)({ outputDir, instance });
140
+ if (snapshots.length === 0) {
141
+ throw new Error("No --from given and no snapshots found for " +
142
+ instance +
143
+ ". Create one with `dove schema pull --snapshot <label>`, or pass --from <path>.");
144
+ }
145
+ fromRef = path_1.default.basename(snapshots[0].dir);
146
+ }
147
+ const toRef = args.to || "live";
148
+ const fromDir = await resolveRefDir({ ref: fromRef, outputDir, instance, scopes, creds });
149
+ const toDir = await resolveRefDir({ ref: toRef, outputDir, instance, scopes, creds });
150
+ const from = await (0, dovetail_schema_1.readSchemaTree)({ dir: fromDir, scope });
151
+ const to = await (0, dovetail_schema_1.readSchemaTree)({ dir: toDir, scope });
152
+ const diff = (0, dovetail_schema_1.diffSchemas)({ from, to, fromRef, toRef, scope });
153
+ // Print with console.log so formatDiff's own severity colors survive (the
154
+ // winston logger re-wraps every line in a single color).
155
+ console.log((0, dovetail_schema_1.formatDiff)(diff, { format }));
156
+ // Non-zero exit on breaking drift, for CI gating.
157
+ process.exitCode = diff.exit_code;
158
+ }
159
+ async function schemaSnapshotsCommand(args) {
160
+ const creds = requireCreds();
161
+ const instance = normalizeInstance(creds.SN_INSTANCE);
162
+ const outputDir = outputDirFor(args);
163
+ const snapshots = await (0, dovetail_schema_1.listSnapshots)({ outputDir, instance });
164
+ if (snapshots.length === 0) {
165
+ Logger_1.logger.info("No snapshots for " +
166
+ instance +
167
+ ". Create one with `dove schema pull --snapshot <label>`.");
168
+ return;
169
+ }
170
+ Logger_1.logger.info("Snapshots for " + instance + ":");
171
+ for (const snap of snapshots) {
172
+ console.log(" " +
173
+ path_1.default.basename(snap.dir) +
174
+ " " +
175
+ snap.total_tables +
176
+ " tables" +
177
+ (snap.label ? " [" + snap.label + "]" : ""));
178
+ }
179
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ // Tests for filesystem-safe folder naming.
3
+ //
4
+ // Dovetail names each record's folder after its ServiceNow display name. Windows
5
+ // forbids `< > : " | ? * \ /`, a trailing space, and a trailing dot in a path
6
+ // component, so a record named "Email Status Change " (trailing space) or an ACL
7
+ // named "x_cadso_work_task.*" cannot be checked out on Windows. toSafeFolderName
8
+ // falls back to the always-safe sys_id for such names; normalizeManifestKeys keeps
9
+ // the on-disk folder name (record.name) equal to the manifest key push looks up by.
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const appUtils_1 = require("../appUtils");
12
+ const rec = (name, sys_id) => ({
13
+ name,
14
+ sys_id,
15
+ files: [{ name: "metaData", type: "json" }],
16
+ });
17
+ describe("toSafeFolderName", () => {
18
+ it("leaves a safe display name unchanged", () => {
19
+ expect((0, appUtils_1.toSafeFolderName)(rec("Send Email", "abc123"))).toBe("Send Email");
20
+ expect((0, appUtils_1.toSafeFolderName)(rec("Prevent delete - WorkGroup", "abc123"))).toBe("Prevent delete - WorkGroup");
21
+ });
22
+ it("falls back to sys_id for a trailing space", () => {
23
+ expect((0, appUtils_1.toSafeFolderName)(rec("Email Status Change ", "fefb8758"))).toBe("fefb8758");
24
+ });
25
+ it("falls back to sys_id for a trailing dot", () => {
26
+ expect((0, appUtils_1.toSafeFolderName)(rec("weird.", "sid1"))).toBe("sid1");
27
+ });
28
+ it("falls back to sys_id for each Windows-illegal character", () => {
29
+ const illegal = ["<", ">", ":", '"', "|", "?", "*", "\\", "/"];
30
+ illegal.forEach((ch) => {
31
+ expect((0, appUtils_1.toSafeFolderName)(rec("name" + ch + "x", "sidX"))).toBe("sidX");
32
+ });
33
+ });
34
+ it("treats the wildcard ACL name as unsafe", () => {
35
+ expect((0, appUtils_1.toSafeFolderName)(rec("x_cadso_work_task.*", "aclsid"))).toBe("aclsid");
36
+ });
37
+ it("falls back to sys_id for an empty name", () => {
38
+ expect((0, appUtils_1.toSafeFolderName)(rec("", "emptysid"))).toBe("emptysid");
39
+ });
40
+ });
41
+ describe("normalizeManifestKeys", () => {
42
+ const buildManifest = (records) => {
43
+ const byKey = {};
44
+ records.forEach((r) => {
45
+ byKey[r.sys_id] = r; // simulate a server manifest keyed by sys_id
46
+ });
47
+ return { scope: "x_cadso_test", tables: { sys_script: { records: byKey } } };
48
+ };
49
+ it("keys unsafe records by sys_id and safe records by name", () => {
50
+ const manifest = buildManifest([
51
+ rec("Send Email", "safe1"),
52
+ rec("Email Status Change ", "fefb8758330ede107b18bc534d5c7bfb"),
53
+ rec("x_cadso_work_task.*", "acl9999"),
54
+ ]);
55
+ (0, appUtils_1.normalizeManifestKeys)(manifest);
56
+ const keys = Object.keys(manifest.tables.sys_script.records);
57
+ expect(keys).toContain("Send Email");
58
+ expect(keys).toContain("fefb8758330ede107b18bc534d5c7bfb");
59
+ expect(keys).toContain("acl9999");
60
+ expect(keys).not.toContain("Email Status Change ");
61
+ expect(keys).not.toContain("x_cadso_work_task.*");
62
+ });
63
+ it("keeps record.name === manifest key for every record (push invariant)", () => {
64
+ const manifest = buildManifest([
65
+ rec("Send Email", "safe1"),
66
+ rec("Email Status Change ", "unsafe1"),
67
+ rec("h:extended", "unsafe2"),
68
+ ]);
69
+ (0, appUtils_1.normalizeManifestKeys)(manifest);
70
+ const records = manifest.tables.sys_script.records;
71
+ Object.keys(records).forEach((key) => {
72
+ expect(records[key].name).toBe(key);
73
+ });
74
+ });
75
+ it("preserves sys_id so push can still resolve the record", () => {
76
+ const manifest = buildManifest([rec("Email Status Change ", "fefb8758")]);
77
+ (0, appUtils_1.normalizeManifestKeys)(manifest);
78
+ const record = manifest.tables.sys_script.records["fefb8758"];
79
+ expect(record).toBeDefined();
80
+ expect(record.sys_id).toBe("fefb8758");
81
+ });
82
+ it("disambiguates duplicate folder names with a sys_id suffix", () => {
83
+ const manifest = buildManifest([
84
+ rec("Duplicate", "1111111111111111"),
85
+ rec("Duplicate", "2222222222222222"),
86
+ ]);
87
+ (0, appUtils_1.normalizeManifestKeys)(manifest);
88
+ const keys = Object.keys(manifest.tables.sys_script.records);
89
+ expect(keys).toContain("Duplicate");
90
+ expect(keys).toContain("Duplicate (22222222)");
91
+ // invariant still holds for the suffixed record
92
+ expect(manifest.tables.sys_script.records["Duplicate (22222222)"].name).toBe("Duplicate (22222222)");
93
+ });
94
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.93",
3
+ "version": "0.0.95",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@tenonhq/dovetail-clickup": "^0.0.10",
42
- "@tenonhq/dovetail-schema": "^0.0.5",
42
+ "@tenonhq/dovetail-schema": "^0.0.9",
43
43
  "axios": "^1.5.1",
44
44
  "axios-cookiejar-support": "^4.0.7",
45
45
  "axios-rate-limit": "^1.3.0",
@@ -1,5 +1,192 @@
1
1
  {
2
2
  "$schema": "dovetail-manifest/v1",
3
- "generated_at": "2026-05-28T00:00:00Z",
4
- "events": []
3
+ "generated_at": "2026-05-29T16:55:37.736Z",
4
+ "events": [
5
+ {
6
+ "$schema": "dovetail-release-event/v1",
7
+ "event_id": "dovetail-dashboard@0.0.33",
8
+ "package": "@tenonhq/dovetail-dashboard",
9
+ "version": "0.0.33",
10
+ "prev_version": "0.0.32",
11
+ "published_at": "2026-05-29T16:55:37.729Z",
12
+ "git": {
13
+ "sha": "3aaef1ba92364c119ece69369c5cc3379da0ed73",
14
+ "tag": "dovetail-dashboard@0.0.33"
15
+ },
16
+ "semver_bump": "patch",
17
+ "commits": [
18
+ {
19
+ "sha": "3aaef1ba9",
20
+ "subject": "fix: legible inline code in Claude Plans dark mode (#120)",
21
+ "pr": 120
22
+ },
23
+ {
24
+ "sha": "92213e1f8",
25
+ "subject": "chore(release): publish 2 packages [skip ci]"
26
+ }
27
+ ],
28
+ "exports_delta": null,
29
+ "narrative": null
30
+ },
31
+ {
32
+ "$schema": "dovetail-release-event/v1",
33
+ "event_id": "dovetail-claude-plans@0.0.20",
34
+ "package": "@tenonhq/dovetail-claude-plans",
35
+ "version": "0.0.20",
36
+ "prev_version": "0.0.19",
37
+ "published_at": "2026-05-29T02:18:11.229Z",
38
+ "git": {
39
+ "sha": "6a12c10733b64732855a8125c84f0d4846ced256",
40
+ "tag": "dovetail-claude-plans@0.0.20"
41
+ },
42
+ "semver_bump": "patch",
43
+ "commits": [
44
+ {
45
+ "sha": "6a12c1073",
46
+ "subject": "feat: claude-plans version history, feature score, selected-state + deep-link hardening (#118)",
47
+ "pr": 118
48
+ },
49
+ {
50
+ "sha": "578d6fb2a",
51
+ "subject": "chore(release): publish 2 packages [skip ci]"
52
+ }
53
+ ],
54
+ "exports_delta": null,
55
+ "narrative": null
56
+ },
57
+ {
58
+ "$schema": "dovetail-release-event/v1",
59
+ "event_id": "dovetail-dashboard@0.0.32",
60
+ "package": "@tenonhq/dovetail-dashboard",
61
+ "version": "0.0.32",
62
+ "prev_version": "0.0.31",
63
+ "published_at": "2026-05-29T02:18:11.229Z",
64
+ "git": {
65
+ "sha": "6a12c10733b64732855a8125c84f0d4846ced256",
66
+ "tag": "dovetail-dashboard@0.0.32"
67
+ },
68
+ "semver_bump": "patch",
69
+ "commits": [
70
+ {
71
+ "sha": "6a12c1073",
72
+ "subject": "feat: claude-plans version history, feature score, selected-state + deep-link hardening (#118)",
73
+ "pr": 118
74
+ },
75
+ {
76
+ "sha": "578d6fb2a",
77
+ "subject": "chore(release): publish 2 packages [skip ci]"
78
+ }
79
+ ],
80
+ "exports_delta": null,
81
+ "narrative": null
82
+ },
83
+ {
84
+ "$schema": "dovetail-release-event/v1",
85
+ "event_id": "dovetail-core@0.0.94",
86
+ "package": "@tenonhq/dovetail-core",
87
+ "version": "0.0.94",
88
+ "prev_version": "0.0.93",
89
+ "published_at": "2026-05-28T23:46:03.175Z",
90
+ "git": {
91
+ "sha": "8a76deffc38892321c92ae538fee3bc6b8f90d76",
92
+ "tag": "dovetail-core@0.0.94"
93
+ },
94
+ "semver_bump": "patch",
95
+ "commits": [
96
+ {
97
+ "sha": "8a76deffc",
98
+ "subject": "fix: filesystem-safe record folder names (sys_id fallback for Windows) (#117)",
99
+ "pr": 117
100
+ },
101
+ {
102
+ "sha": "578d6fb2a",
103
+ "subject": "chore(release): publish 2 packages [skip ci]"
104
+ },
105
+ {
106
+ "sha": "3faeec43f",
107
+ "subject": "chore(release): @tenonhq/dovetail-core@0.0.93 [skip ci]"
108
+ }
109
+ ],
110
+ "exports_delta": null,
111
+ "narrative": null
112
+ },
113
+ {
114
+ "$schema": "dovetail-release-event/v1",
115
+ "event_id": "dovetail-claude-plans@0.0.19",
116
+ "package": "@tenonhq/dovetail-claude-plans",
117
+ "version": "0.0.19",
118
+ "prev_version": "0.0.18",
119
+ "published_at": "2026-05-28T15:54:22.483Z",
120
+ "git": {
121
+ "sha": "0247ef6e75093c95359413725977df07bd5bff70",
122
+ "tag": "dovetail-claude-plans@0.0.19"
123
+ },
124
+ "semver_bump": "patch",
125
+ "commits": [
126
+ {
127
+ "sha": "0247ef6e7",
128
+ "subject": "feat(dashboard): Prompt Lints filters + merged-PR filter for Claude Plans (#116)",
129
+ "pr": 116
130
+ },
131
+ {
132
+ "sha": "5f14a3421",
133
+ "subject": "chore(release): @tenonhq/dovetail-claude-plans@0.0.18 [skip ci]"
134
+ }
135
+ ],
136
+ "exports_delta": null,
137
+ "narrative": null
138
+ },
139
+ {
140
+ "$schema": "dovetail-release-event/v1",
141
+ "event_id": "dovetail-dashboard@0.0.31",
142
+ "package": "@tenonhq/dovetail-dashboard",
143
+ "version": "0.0.31",
144
+ "prev_version": "0.0.30",
145
+ "published_at": "2026-05-28T15:54:22.483Z",
146
+ "git": {
147
+ "sha": "0247ef6e75093c95359413725977df07bd5bff70",
148
+ "tag": "dovetail-dashboard@0.0.31"
149
+ },
150
+ "semver_bump": "patch",
151
+ "commits": [
152
+ {
153
+ "sha": "0247ef6e7",
154
+ "subject": "feat(dashboard): Prompt Lints filters + merged-PR filter for Claude Plans (#116)",
155
+ "pr": 116
156
+ },
157
+ {
158
+ "sha": "c35d549c4",
159
+ "subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.30 [skip ci]"
160
+ }
161
+ ],
162
+ "exports_delta": null,
163
+ "narrative": null
164
+ },
165
+ {
166
+ "$schema": "dovetail-release-event/v1",
167
+ "event_id": "dovetail-core@0.0.93",
168
+ "package": "@tenonhq/dovetail-core",
169
+ "version": "0.0.93",
170
+ "prev_version": "0.0.92",
171
+ "published_at": "2026-05-28T08:23:44.072Z",
172
+ "git": {
173
+ "sha": "f2f4d29e5fbd6197fc501bc803a864ed605cd54a",
174
+ "tag": "dovetail-core@0.0.93"
175
+ },
176
+ "semver_bump": "patch",
177
+ "commits": [
178
+ {
179
+ "sha": "f2f4d29e5",
180
+ "subject": "feat: release manifest emit + dove knowledge-diff (downstream doc sync) (#114)",
181
+ "pr": 114
182
+ },
183
+ {
184
+ "sha": "16e4c0cc2",
185
+ "subject": "chore(release): @tenonhq/dovetail-core@0.0.92 [skip ci]"
186
+ }
187
+ ],
188
+ "exports_delta": null,
189
+ "narrative": null
190
+ }
191
+ ]
5
192
  }