get-tbd 0.1.20 → 0.1.21

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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # tbd
2
2
 
3
+ [![Follow @ojoshe on X](https://img.shields.io/badge/follow_%40ojoshe-black?logo=x&logoColor=white)](https://x.com/ojoshe)
4
+ [![CI](https://github.com/jlevy/tbd/actions/workflows/ci.yml/badge.svg)](https://github.com/jlevy/tbd/actions/workflows/ci.yml)
5
+ [![npm version](https://img.shields.io/npm/v/get-tbd)](https://www.npmjs.com/package/get-tbd)
6
+
3
7
  **Task tracking, spec-driven planning, and knowledge injection for AI coding agents.**
4
8
 
5
9
  **tbd** (short for “To Be Done,” or “TypeScript beads” if you prefer) combines four
package/dist/bin.mjs CHANGED
@@ -6727,17 +6727,23 @@ const Dependency = objectType({
6727
6727
  /**
6728
6728
  * Full issue schema.
6729
6729
  *
6730
+ * Field order is canonical and mirrored by ISSUE_FIELD_ORDER below:
6731
+ * type, id, title, kind, status, priority, version (the "header seven"),
6732
+ * then linkages, assignment, hierarchy, scheduling, provenance,
6733
+ * timestamps, lifecycle, and extensions.
6734
+ *
6730
6735
  * Note: Fields use .nullable() in addition to .optional() because
6731
6736
  * YAML parses `field: null` as JavaScript null, not undefined.
6732
6737
  */
6733
6738
  const IssueSchema = BaseEntity.extend({
6734
6739
  type: literalType("is"),
6735
6740
  title: stringType().min(1).max(500),
6736
- description: stringType().max(5e4).nullable().optional(),
6737
- notes: stringType().max(5e4).nullable().optional(),
6738
6741
  kind: IssueKind.default("task"),
6739
6742
  status: IssueStatus.default("open"),
6740
6743
  priority: Priority.default(2),
6744
+ description: stringType().max(5e4).nullable().optional(),
6745
+ notes: stringType().max(5e4).nullable().optional(),
6746
+ spec_path: stringType().nullable().optional(),
6741
6747
  assignee: stringType().nullable().optional(),
6742
6748
  labels: arrayType(stringType()).default([]),
6743
6749
  dependencies: arrayType(Dependency).default([]),
@@ -6747,8 +6753,7 @@ const IssueSchema = BaseEntity.extend({
6747
6753
  deferred_until: Timestamp.nullable().optional(),
6748
6754
  created_by: stringType().nullable().optional(),
6749
6755
  closed_at: Timestamp.nullable().optional(),
6750
- close_reason: stringType().nullable().optional(),
6751
- spec_path: stringType().nullable().optional()
6756
+ close_reason: stringType().nullable().optional()
6752
6757
  });
6753
6758
  /**
6754
6759
  * Git branch name - restricted to safe characters.
@@ -6862,6 +6867,64 @@ const AtticEntrySchema = objectType({
6862
6867
  * Format: { "a7k2": "01hx5zzkbkactav9wevgemmvrz", ... }
6863
6868
  */
6864
6869
  const IdMappingYamlSchema = recordType(ShortId, Ulid);
6870
+ /**
6871
+ * Canonical field order for issue YAML frontmatter.
6872
+ * (description and notes are body content, not frontmatter)
6873
+ */
6874
+ const ISSUE_FIELD_ORDER = [
6875
+ "type",
6876
+ "id",
6877
+ "title",
6878
+ "kind",
6879
+ "status",
6880
+ "priority",
6881
+ "version",
6882
+ "spec_path",
6883
+ "assignee",
6884
+ "labels",
6885
+ "dependencies",
6886
+ "parent_id",
6887
+ "child_order_hints",
6888
+ "due_date",
6889
+ "deferred_until",
6890
+ "created_by",
6891
+ "created_at",
6892
+ "updated_at",
6893
+ "closed_at",
6894
+ "close_reason",
6895
+ "extensions"
6896
+ ];
6897
+ /**
6898
+ * Canonical field order for config YAML.
6899
+ */
6900
+ const CONFIG_FIELD_ORDER = [
6901
+ "tbd_format",
6902
+ "tbd_version",
6903
+ "display",
6904
+ "sync",
6905
+ "settings",
6906
+ "docs_cache"
6907
+ ];
6908
+ /**
6909
+ * Canonical field order for attic entry YAML.
6910
+ */
6911
+ const ATTIC_ENTRY_FIELD_ORDER = [
6912
+ "entity_id",
6913
+ "timestamp",
6914
+ "field",
6915
+ "lost_value",
6916
+ "winner_source",
6917
+ "loser_source",
6918
+ "context"
6919
+ ];
6920
+ /**
6921
+ * Canonical field order for local state YAML.
6922
+ */
6923
+ const LOCAL_STATE_FIELD_ORDER = [
6924
+ "last_sync_at",
6925
+ "last_doc_sync_at",
6926
+ "welcome_seen"
6927
+ ];
6865
6928
 
6866
6929
  //#endregion
6867
6930
  //#region src/lib/types.ts
@@ -13583,6 +13646,76 @@ const PAGINATION_LINE_THRESHOLD = 50;
13583
13646
  */
13584
13647
  const PARENT_CONTEXT_MAX_LINES = 50;
13585
13648
 
13649
+ //#endregion
13650
+ //#region src/lib/comparison-chain.ts
13651
+ const defaultCompare = (a, b) => {
13652
+ if (typeof a === "string" && typeof b === "string") return a.localeCompare(b);
13653
+ if (a < b) return -1;
13654
+ if (a > b) return 1;
13655
+ return 0;
13656
+ };
13657
+ const nullLastCompare = (a, b) => {
13658
+ if (a == null) return b == null ? 0 : 1;
13659
+ if (b == null) return -1;
13660
+ return defaultCompare(a, b);
13661
+ };
13662
+ const nullFirstCompare = (a, b) => {
13663
+ if (a == null) return b == null ? 0 : -1;
13664
+ if (b == null) return 1;
13665
+ return defaultCompare(a, b);
13666
+ };
13667
+ /**
13668
+ * A Google Guava-style comparison chain to make complex sorting, such as secondary
13669
+ * sorts, significantly easier.
13670
+ *
13671
+ * For example:
13672
+ *
13673
+ * items.sort(comparisonChain<Item>()
13674
+ * .compare(item => item.title, ordering.nullsLast)
13675
+ * .compare(item => item.url)
13676
+ * .result());
13677
+ */
13678
+ const comparisonChain = () => {
13679
+ let compare = () => 0;
13680
+ const chain = {
13681
+ compare: (selector, comparator = defaultCompare) => {
13682
+ const prevCompare = compare;
13683
+ compare = (a, b) => prevCompare(a, b) || comparator(selector(a), selector(b));
13684
+ return chain;
13685
+ },
13686
+ result: () => compare
13687
+ };
13688
+ return chain;
13689
+ };
13690
+ /**
13691
+ * Reverse a comparator's ordering.
13692
+ */
13693
+ const reverse = (comparator) => (a, b) => comparator(b, a);
13694
+ /**
13695
+ * Create a comparator that sorts values in a manually specified order.
13696
+ * Values not in the order array are placed at the end.
13697
+ */
13698
+ const manualOrderComparator = (order) => {
13699
+ const orderMap = new Map(order.map((value, index) => [value, index]));
13700
+ return (a, b) => {
13701
+ const indexA = orderMap.get(a);
13702
+ const indexB = orderMap.get(b);
13703
+ if (indexA === void 0) return indexB === void 0 ? 0 : 1;
13704
+ if (indexB === void 0) return -1;
13705
+ return indexA - indexB;
13706
+ };
13707
+ };
13708
+ /**
13709
+ * Common ordering strategies for use with comparisonChain.
13710
+ */
13711
+ const ordering = {
13712
+ nullsLast: nullLastCompare,
13713
+ nullsFirst: nullFirstCompare,
13714
+ default: defaultCompare,
13715
+ reversed: reverse(defaultCompare),
13716
+ manual: manualOrderComparator
13717
+ };
13718
+
13586
13719
  //#endregion
13587
13720
  //#region src/utils/yaml-utils.ts
13588
13721
  /**
@@ -13626,6 +13759,17 @@ function stringifyYamlCompact(data) {
13626
13759
  return (0, import_dist$2.stringify)(data, YAML_STRINGIFY_OPTIONS_COMPACT);
13627
13760
  }
13628
13761
  /**
13762
+ * Create a new object with keys sorted according to a manual field ordering.
13763
+ * Uses ordering.manual() from comparison-chain.ts, so fields not in the
13764
+ * order array are placed at the end.
13765
+ */
13766
+ function sortKeys(obj, fieldOrder) {
13767
+ const keyComparator = ordering.manual(fieldOrder);
13768
+ const sorted = {};
13769
+ for (const key of Object.keys(obj).sort(keyComparator)) sorted[key] = obj[key];
13770
+ return sorted;
13771
+ }
13772
+ /**
13629
13773
  * Error thrown when YAML content contains unresolved merge conflict markers.
13630
13774
  */
13631
13775
  var MergeConflictError = class extends Error {
@@ -13858,13 +14002,12 @@ function parseIssue(content) {
13858
14002
  */
13859
14003
  function serializeIssue(issue) {
13860
14004
  const { description, notes, ...metadata } = issue;
13861
- const sortedMetadata = {};
13862
- for (const key of Object.keys(metadata).sort()) sortedMetadata[key] = metadata[key];
13863
14005
  const parts = [
13864
14006
  "---",
13865
- stringifyYaml(sortedMetadata, {
14007
+ stringifyYaml(sortKeys(metadata, ISSUE_FIELD_ORDER), {
13866
14008
  lineWidth: 0,
13867
- nullStr: "null"
14009
+ nullStr: "null",
14010
+ sortMapEntries: false
13868
14011
  }).trim(),
13869
14012
  "---"
13870
14013
  ];
@@ -13884,7 +14027,7 @@ function serializeIssue(issue) {
13884
14027
  * Package version, derived from git at build time.
13885
14028
  * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
13886
14029
  */
13887
- const VERSION$1 = "0.1.20";
14030
+ const VERSION$1 = "0.1.21";
13888
14031
 
13889
14032
  //#endregion
13890
14033
  //#region src/cli/lib/version.ts
@@ -97806,7 +97949,10 @@ async function readConfigWithMigration(baseDir) {
97806
97949
  */
97807
97950
  async function writeConfig(baseDir, config) {
97808
97951
  const configPath = join(baseDir, CONFIG_FILE);
97809
- let content = stringifyYaml(config, { lineWidth: 0 });
97952
+ let content = stringifyYaml(sortKeys(config, CONFIG_FIELD_ORDER), {
97953
+ lineWidth: 0,
97954
+ sortMapEntries: false
97955
+ });
97810
97956
  if (config.docs_cache && Object.keys(config.docs_cache).length > 0) content = content.replace("docs_cache:", "# Documentation cache configuration.\n# files: Maps destination paths (relative to .tbd/docs/) to source locations.\n# Sources can be:\n# - internal: prefix for bundled docs (e.g., \"internal:shortcuts/standard/code-review-and-commit.md\")\n# - Full URL for external docs (e.g., \"https://raw.githubusercontent.com/org/repo/main/file.md\")\n# lookup_path: Search paths for doc lookup (like shell $PATH). Earlier paths take precedence.\n#\n# To sync docs: tbd sync --docs\n# To check status: tbd sync --status\n#\n# Auto-sync: Docs are automatically synced when stale (default: every 24 hours).\n# Configure with settings.doc_auto_sync_hours (0 = disabled).\ndocs_cache:");
97811
97957
  await writeFile(configPath, content);
97812
97958
  }
@@ -97879,7 +98025,10 @@ async function writeLocalState(baseDir, state) {
97879
98025
  } catch {
97880
98026
  throw new Error(`Cannot write state: .tbd/ directory does not exist at ${baseDir}. Run 'tbd init' first or ensure the correct tbd root is being used.`);
97881
98027
  }
97882
- await writeFile(join(baseDir, STATE_FILE), stringifyYaml(state, { lineWidth: 0 }));
98028
+ await writeFile(join(baseDir, STATE_FILE), stringifyYaml(sortKeys(state, LOCAL_STATE_FIELD_ORDER), {
98029
+ lineWidth: 0,
98030
+ sortMapEntries: false
98031
+ }));
97883
98032
  }
97884
98033
  /**
97885
98034
  * Update specific fields in local state (merge with existing).
@@ -98489,6 +98638,26 @@ const FIELD_STRATEGIES = {
98489
98638
  extensions: "lww"
98490
98639
  };
98491
98640
  /**
98641
+ * Fields that are metadata-only and should be ignored when checking
98642
+ * for substantive changes between issues. These fields change on every
98643
+ * merge operation and don't represent meaningful content changes.
98644
+ */
98645
+ const METADATA_ONLY_FIELDS = new Set(["version", "updated_at"]);
98646
+ /**
98647
+ * Check if two issues are substantively equal, ignoring metadata fields
98648
+ * (version, updated_at) that change on every merge.
98649
+ *
98650
+ * This prevents trivial timestamp/version bumps from being treated as
98651
+ * real changes during outbox saves and sync operations.
98652
+ */
98653
+ function issuesSubstantivelyEqual(a, b) {
98654
+ for (const key of Object.keys(FIELD_STRATEGIES)) {
98655
+ if (METADATA_ONLY_FIELDS.has(key)) continue;
98656
+ if (!deepEqual(a[key], b[key])) return false;
98657
+ }
98658
+ return true;
98659
+ }
98660
+ /**
98492
98661
  * Deep equality check for values.
98493
98662
  */
98494
98663
  function deepEqual(a, b) {
@@ -98596,6 +98765,11 @@ function mergeIssues(base, local, remote) {
98596
98765
  break;
98597
98766
  }
98598
98767
  }
98768
+ const latest = local.version >= remote.version ? local : remote;
98769
+ if (issuesSubstantivelyEqual(merged, latest)) return {
98770
+ merged: { ...latest },
98771
+ conflicts
98772
+ };
98599
98773
  merged.version = Math.max(local.version, remote.version) + 1;
98600
98774
  merged.updated_at = now();
98601
98775
  return {
@@ -99233,6 +99407,26 @@ function randomChar(prng) {
99233
99407
  const randomPosition = Math.floor(prng() * ENCODING_LEN) % ENCODING_LEN;
99234
99408
  return ENCODING.charAt(randomPosition);
99235
99409
  }
99410
+ function replaceCharAt(str, index, char) {
99411
+ if (index > str.length - 1) return str;
99412
+ return str.substr(0, index) + char + str.substr(index + 1);
99413
+ }
99414
+ function incrementBase32(str) {
99415
+ let done = void 0, index = str.length, char, charIndex, output = str;
99416
+ const maxCharIndex = ENCODING_LEN - 1;
99417
+ while (!done && index-- >= 0) {
99418
+ char = output[index];
99419
+ charIndex = ENCODING.indexOf(char);
99420
+ if (charIndex === -1) throw new ULIDError(ULIDErrorCode.Base32IncorrectEncoding, "Incorrectly encoded string");
99421
+ if (charIndex === maxCharIndex) {
99422
+ output = replaceCharAt(output, index, ENCODING[0]);
99423
+ continue;
99424
+ }
99425
+ done = replaceCharAt(output, index, ENCODING[charIndex + 1]);
99426
+ }
99427
+ if (typeof done === "string") return done;
99428
+ throw new ULIDError(ULIDErrorCode.Base32IncorrectEncoding, "Failed incrementing string");
99429
+ }
99236
99430
  /**
99237
99431
  * Detect the best PRNG (pseudo-random number generator)
99238
99432
  * @param root The root to check from (global/window)
@@ -99285,16 +99479,27 @@ function inWebWorker() {
99285
99479
  return typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
99286
99480
  }
99287
99481
  /**
99288
- * Generate a ULID
99289
- * @param seedTime Optional time seed
99290
- * @param prng Optional PRNG function
99291
- * @returns A ULID string
99482
+ * Create a ULID factory to generate monotonically-increasing
99483
+ * ULIDs
99484
+ * @param prng The PRNG to use
99485
+ * @returns A ulid factory
99292
99486
  * @example
99487
+ * const ulid = monotonicFactory();
99293
99488
  * ulid(); // "01HNZXD07M5CEN5XA66EMZSRZW"
99294
99489
  */
99295
- function ulid(seedTime, prng) {
99490
+ function monotonicFactory(prng) {
99296
99491
  const currentPRNG = prng || detectPRNG();
99297
- return encodeTime(!seedTime || isNaN(seedTime) ? Date.now() : seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN, currentPRNG);
99492
+ let lastTime = 0, lastRandom;
99493
+ return function _ulid(seedTime) {
99494
+ const seed = !seedTime || isNaN(seedTime) ? Date.now() : seedTime;
99495
+ if (seed <= lastTime) {
99496
+ const incrementedRandom = lastRandom = incrementBase32(lastRandom);
99497
+ return encodeTime(lastTime, TIME_LEN) + incrementedRandom;
99498
+ }
99499
+ lastTime = seed;
99500
+ const newRandom = lastRandom = encodeRandom(RANDOM_LEN, currentPRNG);
99501
+ return encodeTime(seed, TIME_LEN) + newRandom;
99502
+ };
99298
99503
  }
99299
99504
 
99300
99505
  //#endregion
@@ -99310,6 +99515,7 @@ function ulid(seedTime, prng) {
99310
99515
  *
99311
99516
  * See: tbd-design.md §2.5 ID Generation
99312
99517
  */
99518
+ const ulid = monotonicFactory();
99313
99519
  /**
99314
99520
  * Cast a string to InternalIssueId after validation.
99315
99521
  * Use this when you've validated that a string is a valid internal ID.
@@ -100214,76 +100420,6 @@ async function loadFullContext(command) {
100214
100420
  };
100215
100421
  }
100216
100422
 
100217
- //#endregion
100218
- //#region src/lib/comparison-chain.ts
100219
- const defaultCompare = (a, b) => {
100220
- if (typeof a === "string" && typeof b === "string") return a.localeCompare(b);
100221
- if (a < b) return -1;
100222
- if (a > b) return 1;
100223
- return 0;
100224
- };
100225
- const nullLastCompare = (a, b) => {
100226
- if (a == null) return b == null ? 0 : 1;
100227
- if (b == null) return -1;
100228
- return defaultCompare(a, b);
100229
- };
100230
- const nullFirstCompare = (a, b) => {
100231
- if (a == null) return b == null ? 0 : -1;
100232
- if (b == null) return 1;
100233
- return defaultCompare(a, b);
100234
- };
100235
- /**
100236
- * A Google Guava-style comparison chain to make complex sorting, such as secondary
100237
- * sorts, significantly easier.
100238
- *
100239
- * For example:
100240
- *
100241
- * items.sort(comparisonChain<Item>()
100242
- * .compare(item => item.title, ordering.nullsLast)
100243
- * .compare(item => item.url)
100244
- * .result());
100245
- */
100246
- const comparisonChain = () => {
100247
- let compare = () => 0;
100248
- const chain = {
100249
- compare: (selector, comparator = defaultCompare) => {
100250
- const prevCompare = compare;
100251
- compare = (a, b) => prevCompare(a, b) || comparator(selector(a), selector(b));
100252
- return chain;
100253
- },
100254
- result: () => compare
100255
- };
100256
- return chain;
100257
- };
100258
- /**
100259
- * Reverse a comparator's ordering.
100260
- */
100261
- const reverse = (comparator) => (a, b) => comparator(b, a);
100262
- /**
100263
- * Create a comparator that sorts values in a manually specified order.
100264
- * Values not in the order array are placed at the end.
100265
- */
100266
- const manualOrderComparator = (order) => {
100267
- const orderMap = new Map(order.map((value, index) => [value, index]));
100268
- return (a, b) => {
100269
- const indexA = orderMap.get(a);
100270
- const indexB = orderMap.get(b);
100271
- if (indexA === void 0) return indexB === void 0 ? 0 : 1;
100272
- if (indexB === void 0) return -1;
100273
- return indexA - indexB;
100274
- };
100275
- };
100276
- /**
100277
- * Common ordering strategies for use with comparisonChain.
100278
- */
100279
- const ordering = {
100280
- nullsLast: nullLastCompare,
100281
- nullsFirst: nullFirstCompare,
100282
- default: defaultCompare,
100283
- reversed: reverse(defaultCompare),
100284
- manual: manualOrderComparator
100285
- };
100286
-
100287
100423
  //#endregion
100288
100424
  //#region src/lib/status.ts
100289
100425
  /**
@@ -100788,14 +100924,10 @@ var ListHandler = class extends BaseCommand {
100788
100924
  return true;
100789
100925
  });
100790
100926
  }
100791
- sortIssues(issues, sortField, mapping) {
100792
- const getShortId = (issue) => {
100793
- const ulid = extractUlidFromInternalId(issue.id);
100794
- return mapping.ulidToShort.get(ulid) ?? ulid;
100795
- };
100927
+ sortIssues(issues, sortField, _mapping) {
100796
100928
  const primarySelector = sortField === "created" ? (i) => new Date(i.created_at).getTime() : sortField === "updated" ? (i) => new Date(i.updated_at).getTime() : (i) => i.priority;
100797
100929
  const primaryOrdering = sortField === "created" || sortField === "updated" ? ordering.reversed : ordering.default;
100798
- return [...issues].sort(comparisonChain().compare(primarySelector, primaryOrdering).compare(getShortId, (a, b) => naturalCompare(a, b)).result());
100930
+ return [...issues].sort(comparisonChain().compare(primarySelector, primaryOrdering).compare((i) => extractUlidFromInternalId(i.id)).result());
100799
100931
  }
100800
100932
  };
100801
100933
  const listCommand = new Command("list").description("List issues").option("--status <status>", "Filter: open, in_progress, blocked, deferred, closed").option("--all", "Include closed issues").option("--type <type>", "Filter: bug, feature, task, epic").option("--priority <0-4>", "Filter by priority").option("--assignee <name>", "Filter by assignee").option("--label <label>", "Filter by label (repeatable)", (val, prev = []) => [...prev, val]).option("--parent <id>", "List children of parent").option("--spec <path>", "Filter by spec path (matches full path, partial path suffix, or filename)").option("--deferred", "Show only deferred issues").option("--defer-before <date>", "Deferred before date").option("--sort <field>", "Sort by: priority, created, updated", "priority").option("--limit <n>", "Limit results").option("--count", "Output only the count of matching issues").option("--long", "Show descriptions").option("--pretty", "Show tree view with parent-child relationships").option("--specs", "Group output by linked spec").action(async (options, command) => {
@@ -102380,11 +102512,15 @@ async function syncDocsWithDefaults(tbdRoot, options = {}) {
102380
102512
  *
102381
102513
  * An issue is considered "updated" if:
102382
102514
  * - It doesn't exist in the remote (new issue)
102383
- * - Its content differs from the remote version (modified issue)
102515
+ * - Its substantive content differs from the remote version (modified issue)
102516
+ *
102517
+ * Uses issuesSubstantivelyEqual to ignore metadata-only changes (version, updated_at)
102518
+ * that don't represent meaningful content changes. This prevents trivial timestamp
102519
+ * bumps from causing thousands of issues to be saved to the outbox.
102384
102520
  *
102385
102521
  * @param localIssues - Issues from the local worktree
102386
102522
  * @param remoteIssues - Issues from the remote tbd-sync branch
102387
- * @returns Issues that are new or modified compared to remote
102523
+ * @returns Issues that are new or substantively modified compared to remote
102388
102524
  */
102389
102525
  function getUpdatedIssues(localIssues, remoteIssues) {
102390
102526
  const remoteById = /* @__PURE__ */ new Map();
@@ -102392,7 +102528,7 @@ function getUpdatedIssues(localIssues, remoteIssues) {
102392
102528
  return localIssues.filter((local) => {
102393
102529
  const remote = remoteById.get(local.id);
102394
102530
  if (!remote) return true;
102395
- return !deepEqual(local, remote);
102531
+ return !issuesSubstantivelyEqual(local, remote);
102396
102532
  });
102397
102533
  }
102398
102534
  /**
@@ -102447,7 +102583,7 @@ async function saveConflictToAttic(atticDir, conflict, winnerSource) {
102447
102583
  }
102448
102584
  };
102449
102585
  const safeTimestamp = timestamp.replace(/:/g, "-");
102450
- await writeFile(join(atticDir, `${conflict.issue_id}_${safeTimestamp}_${conflict.field}.yml`), stringifyYaml(entry));
102586
+ await writeFile(join(atticDir, `${conflict.issue_id}_${safeTimestamp}_${conflict.field}.yml`), stringifyYaml(sortKeys(entry, ATTIC_ENTRY_FIELD_ORDER), { sortMapEntries: false }));
102451
102587
  }
102452
102588
  /**
102453
102589
  * Get the target/source directory for workspace operations.
@@ -102493,17 +102629,23 @@ async function saveToWorkspace(tbdRoot, dataSyncDir, options) {
102493
102629
  log.info(`Loaded ${totalSource} issue(s) from worktree`);
102494
102630
  let sourceIssues = allSourceIssues;
102495
102631
  const isUpdatesOnly = options.updatesOnly ?? options.outbox;
102496
- if (isUpdatesOnly) try {
102497
- log.progress("Fetching remote for comparison...");
102498
- await git("-C", tbdRoot, "fetch", "origin", "tbd-sync");
102499
- log.debug("Fetch succeeded, reading remote issues...");
102500
- const remoteIssues = await readRemoteIssues(tbdRoot, "origin", "tbd-sync");
102501
- log.info(`Loaded ${remoteIssues.length} remote issue(s) for comparison`);
102502
- sourceIssues = getUpdatedIssues(allSourceIssues, remoteIssues);
102503
- log.info(`Filtered to ${sourceIssues.length} updated issue(s) (of ${totalSource} total)`);
102504
- } catch (fetchError) {
102505
- const fetchMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
102506
- log.info(`Fetch failed (${fetchMsg}), falling back to saving all ${totalSource} issues`);
102632
+ if (isUpdatesOnly) {
102633
+ try {
102634
+ log.progress("Fetching remote for comparison...");
102635
+ await git("-C", tbdRoot, "fetch", "origin", "tbd-sync");
102636
+ log.debug("Fetch succeeded");
102637
+ } catch (fetchError) {
102638
+ const fetchMsg = fetchError instanceof Error ? fetchError.message : String(fetchError);
102639
+ log.info(`Fetch failed (${fetchMsg}), using cached remote state for comparison`);
102640
+ }
102641
+ try {
102642
+ const remoteIssues = await readRemoteIssues(tbdRoot, "origin", "tbd-sync");
102643
+ log.info(`Loaded ${remoteIssues.length} remote issue(s) for comparison`);
102644
+ sourceIssues = getUpdatedIssues(allSourceIssues, remoteIssues);
102645
+ log.info(`Filtered to ${sourceIssues.length} updated issue(s) (of ${totalSource} total)`);
102646
+ } catch {
102647
+ log.info(`No remote state available, falling back to saving all ${totalSource} issues`);
102648
+ }
102507
102649
  }
102508
102650
  let saved = 0;
102509
102651
  let conflicts = 0;