@rpamis/comet 0.4.0-beta.8 → 0.4.0-beta.9

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 (128) hide show
  1. package/README.md +1 -1
  2. package/assets/manifest.json +1 -1
  3. package/assets/skills/comet/reference/comet-yaml-fields.md +2 -2
  4. package/assets/skills/comet/scripts/comet-entry-runtime.mjs +95 -4
  5. package/assets/skills/comet/scripts/comet-hook-router.mjs +224 -35
  6. package/assets/skills/comet/scripts/comet-runtime.mjs +218 -102
  7. package/assets/skills/comet-archive/SKILL.md +29 -20
  8. package/assets/skills/comet-native/SKILL.md +23 -15
  9. package/assets/skills/comet-native/reference/artifacts.md +19 -4
  10. package/assets/skills/comet-native/reference/commands.md +8 -2
  11. package/assets/skills/comet-native/reference/recovery.md +1 -1
  12. package/assets/skills/comet-native/scripts/comet-native-runtime.mjs +1052 -458
  13. package/assets/skills-zh/comet/reference/comet-yaml-fields.md +2 -2
  14. package/assets/skills-zh/comet-archive/SKILL.md +29 -20
  15. package/assets/skills-zh/comet-native/SKILL.md +23 -15
  16. package/assets/skills-zh/comet-native/reference/artifacts.md +20 -5
  17. package/assets/skills-zh/comet-native/reference/commands.md +8 -2
  18. package/assets/skills-zh/comet-native/reference/recovery.md +1 -1
  19. package/dist/app/commands/init.d.ts.map +1 -1
  20. package/dist/app/commands/init.js +3 -6
  21. package/dist/app/commands/init.js.map +1 -1
  22. package/dist/domains/comet-entry/current-selection.d.ts.map +1 -1
  23. package/dist/domains/comet-entry/current-selection.js +6 -8
  24. package/dist/domains/comet-entry/current-selection.js.map +1 -1
  25. package/dist/domains/comet-entry/project-status.d.ts.map +1 -1
  26. package/dist/domains/comet-entry/project-status.js +5 -1
  27. package/dist/domains/comet-entry/project-status.js.map +1 -1
  28. package/dist/domains/comet-native/native-build-evidence.js +2 -2
  29. package/dist/domains/comet-native/native-build-evidence.js.map +1 -1
  30. package/dist/domains/comet-native/native-change.d.ts +4 -2
  31. package/dist/domains/comet-native/native-change.d.ts.map +1 -1
  32. package/dist/domains/comet-native/native-change.js +18 -4
  33. package/dist/domains/comet-native/native-change.js.map +1 -1
  34. package/dist/domains/comet-native/native-check-receipt.d.ts.map +1 -1
  35. package/dist/domains/comet-native/native-check-receipt.js +5 -3
  36. package/dist/domains/comet-native/native-check-receipt.js.map +1 -1
  37. package/dist/domains/comet-native/native-cli.d.ts.map +1 -1
  38. package/dist/domains/comet-native/native-cli.js +120 -16
  39. package/dist/domains/comet-native/native-cli.js.map +1 -1
  40. package/dist/domains/comet-native/native-config.d.ts +5 -1
  41. package/dist/domains/comet-native/native-config.d.ts.map +1 -1
  42. package/dist/domains/comet-native/native-config.js +78 -0
  43. package/dist/domains/comet-native/native-config.js.map +1 -1
  44. package/dist/domains/comet-native/native-continuation.d.ts +2 -1
  45. package/dist/domains/comet-native/native-continuation.d.ts.map +1 -1
  46. package/dist/domains/comet-native/native-continuation.js +13 -5
  47. package/dist/domains/comet-native/native-continuation.js.map +1 -1
  48. package/dist/domains/comet-native/native-diagnostics.d.ts +7 -3
  49. package/dist/domains/comet-native/native-diagnostics.d.ts.map +1 -1
  50. package/dist/domains/comet-native/native-diagnostics.js +19 -9
  51. package/dist/domains/comet-native/native-diagnostics.js.map +1 -1
  52. package/dist/domains/comet-native/native-file-identity.d.ts +2 -8
  53. package/dist/domains/comet-native/native-file-identity.d.ts.map +1 -1
  54. package/dist/domains/comet-native/native-file-identity.js +1 -20
  55. package/dist/domains/comet-native/native-file-identity.js.map +1 -1
  56. package/dist/domains/comet-native/native-findings.d.ts.map +1 -1
  57. package/dist/domains/comet-native/native-findings.js +19 -1
  58. package/dist/domains/comet-native/native-findings.js.map +1 -1
  59. package/dist/domains/comet-native/native-guards.d.ts +2 -1
  60. package/dist/domains/comet-native/native-guards.d.ts.map +1 -1
  61. package/dist/domains/comet-native/native-guards.js +14 -0
  62. package/dist/domains/comet-native/native-guards.js.map +1 -1
  63. package/dist/domains/comet-native/native-lock.d.ts.map +1 -1
  64. package/dist/domains/comet-native/native-lock.js +24 -32
  65. package/dist/domains/comet-native/native-lock.js.map +1 -1
  66. package/dist/domains/comet-native/native-root-move.d.ts.map +1 -1
  67. package/dist/domains/comet-native/native-root-move.js +2 -0
  68. package/dist/domains/comet-native/native-root-move.js.map +1 -1
  69. package/dist/domains/comet-native/native-snapshot.d.ts +5 -1
  70. package/dist/domains/comet-native/native-snapshot.d.ts.map +1 -1
  71. package/dist/domains/comet-native/native-snapshot.js +218 -2
  72. package/dist/domains/comet-native/native-snapshot.js.map +1 -1
  73. package/dist/domains/comet-native/native-transitions.d.ts +2 -1
  74. package/dist/domains/comet-native/native-transitions.d.ts.map +1 -1
  75. package/dist/domains/comet-native/native-transitions.js +34 -11
  76. package/dist/domains/comet-native/native-transitions.js.map +1 -1
  77. package/dist/domains/comet-native/native-types.d.ts +16 -0
  78. package/dist/domains/comet-native/native-types.d.ts.map +1 -1
  79. package/dist/domains/comet-native/native-verification-runtime.d.ts.map +1 -1
  80. package/dist/domains/comet-native/native-verification-runtime.js +5 -3
  81. package/dist/domains/comet-native/native-verification-runtime.js.map +1 -1
  82. package/dist/domains/comet-native/native-verification-scope.d.ts +2 -1
  83. package/dist/domains/comet-native/native-verification-scope.d.ts.map +1 -1
  84. package/dist/domains/comet-native/native-verification-scope.js +6 -1
  85. package/dist/domains/comet-native/native-verification-scope.js.map +1 -1
  86. package/dist/domains/dashboard/native-collector.d.ts.map +1 -1
  87. package/dist/domains/dashboard/native-collector.js +4 -1
  88. package/dist/domains/dashboard/native-collector.js.map +1 -1
  89. package/dist/domains/skill/platform-install.d.ts.map +1 -1
  90. package/dist/domains/skill/platform-install.js +9 -0
  91. package/dist/domains/skill/platform-install.js.map +1 -1
  92. package/dist/domains/skill/project-instructions.d.ts.map +1 -1
  93. package/dist/domains/skill/project-instructions.js +4 -2
  94. package/dist/domains/skill/project-instructions.js.map +1 -1
  95. package/dist/domains/workflow-contract/project-config.d.ts +1 -1
  96. package/dist/domains/workflow-contract/project-config.d.ts.map +1 -1
  97. package/dist/domains/workflow-contract/project-config.js +24 -2
  98. package/dist/domains/workflow-contract/project-config.js.map +1 -1
  99. package/dist/platform/fs/file-identity.d.ts +9 -0
  100. package/dist/platform/fs/file-identity.d.ts.map +1 -0
  101. package/dist/platform/fs/file-identity.js +21 -0
  102. package/dist/platform/fs/file-identity.js.map +1 -0
  103. package/dist/platform/fs/file-system.d.ts.map +1 -1
  104. package/dist/platform/fs/file-system.js +13 -9
  105. package/dist/platform/fs/file-system.js.map +1 -1
  106. package/dist/platform/fs/race-safe-read.d.ts +47 -0
  107. package/dist/platform/fs/race-safe-read.d.ts.map +1 -0
  108. package/dist/platform/fs/race-safe-read.js +132 -0
  109. package/dist/platform/fs/race-safe-read.js.map +1 -0
  110. package/eval/README.md +1 -1
  111. package/eval/local/tasks/comet-native-clarification-depth/environment/Dockerfile +27 -0
  112. package/eval/local/tasks/comet-native-clarification-depth/environment/test_wordcount.py +25 -0
  113. package/eval/local/tasks/comet-native-clarification-depth/environment/wordcount.py +36 -0
  114. package/eval/local/tasks/comet-native-clarification-depth/instruction.md +17 -0
  115. package/eval/local/tasks/comet-native-clarification-depth/task.toml +47 -0
  116. package/eval/local/tasks/comet-native-clarification-depth/validation/test_native_clarification_depth.py +618 -0
  117. package/eval/local/tasks/comet-native-clarification-modes/validation/test_native_clarification_modes.py +1 -1
  118. package/eval/local/tasks/index.yaml +6 -0
  119. package/eval/local/tests/conftest.py +146 -2
  120. package/eval/local/tests/scaffold/test_conftest_helpers.py +188 -1
  121. package/eval/local/tests/scaffold/test_tasks.py +440 -8
  122. package/eval/local/tests/scaffold/test_utils.py +114 -1
  123. package/eval/local/tests/tasks/test_tasks.py +2 -0
  124. package/eval/scaffold/python/aligned_comparison.py +1 -0
  125. package/eval/scaffold/python/tasks.py +2 -0
  126. package/eval/scaffold/shell/docker.sh +1 -1
  127. package/eval/scaffold/shell/run-claude-loop.sh +25 -2
  128. package/package.json +1 -1
package/README.md CHANGED
@@ -837,7 +837,7 @@ Track our development progress and upcoming features on the [Comet Roadmap](http
837
837
 
838
838
  ## Star History
839
839
 
840
- [![Star History Chart](https://api.star-history.com/chart?repos=rpamis/comet&type=date&legend=top-left&sealed_token=vRfs1efclBxdyNz7q0GUHGe9kUU96aSUCa1eHI8CEWehNHvZoop01eCjM0jpVMgeYBjvnGBcd0OUHnhQBC8p6gXP2Drpmo3pLXl_r0prKSuNW6OTqddOBCgaPtSt_KDlRgXjHZhx94_zcXWkIg5HOJEjPq4Qp2TMEa6inFxm7TixQQRIdPgKw2Z00nie)](https://www.star-history.com/?repos=rpamis%2Fcomet&type=date&legend=top-left)
840
+ [![Star History Chart](https://api.star-history.com/chart?repos=rpamis/comet&type=date&legend=top-left&sealed_token=lB84nxf4Emtuo-2F58IUt3ya0FIbY3dTDwnIl9UFA3u_IvEUqaI6dpYXmPItlNJcT00eAFXz5xyZN9TsoBh2la54tkgUbdncTIoFIJco7TO-Vrt-m9SFxw)](https://www.star-history.com/?repos=rpamis%2Fcomet&type=date&legend=top-left)
841
841
 
842
842
  ## Contributors
843
843
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.0-beta.8",
2
+ "version": "0.4.0-beta.9",
3
3
  "skills": [
4
4
  "comet/SKILL.md",
5
5
  "comet-classic/SKILL.md",
@@ -55,10 +55,10 @@ archived: false
55
55
  | `verify_result` | `pending`, `pass`, or `fail` |
56
56
  | `verify_failures` | Machine-owned consecutive verification failure count. `verify-fail` increments it; `verify-pass` or `archive-reopen` resets it to `0`. At `3`, the next failure requires the retry-limit strategy decision |
57
57
  | `verification_report` | Verification report file path; must point to an existing file before verify passes |
58
- | `branch_status` | `pending` or `handled`; keep pending through verify/archive, then set handled after the archive commit and selected branch handling complete |
58
+ | `branch_status` | `pending` or `handled`; keep `pending` through verify. After the user confirms immediate remote delivery before archive, set `handled` when archive finishes and include it in the only archive commit. `handled` means only that the delivery method is confirmed, not that push or PR creation succeeded; clear current selection and report workflow completion only after remote operations succeed |
59
59
  | `created_at` | Change creation date (auto-written at init), format `YYYY-MM-DD` |
60
60
  | `verified_at` | Verification pass timestamp; may be empty |
61
- | `archive_confirmation` | `null`, `pending`, or `confirmed`. `verify-pass` writes `pending` when entering the archive phase; after the user selects "Confirm archive" in `/comet-archive`, the `archive-confirm` transition writes `confirmed`; `archive-reopen` clears the field so an earlier confirmation cannot be reused |
61
+ | `archive_confirmation` | `null`, `pending`, or `confirmed`. `verify-pass` writes `pending` when entering the archive phase; after the user selects either "confirm archive and deliver remotely now" choice in `/comet-archive`, the `archive-confirm` transition writes `confirmed`; `archive-reopen` clears the field so an earlier confirmation cannot be reused |
62
62
  | `archived` | Whether the change has been archived |
63
63
 
64
64
  ## Optional Fields
@@ -7375,11 +7375,11 @@ import path3 from "path";
7375
7375
  // domains/workflow-contract/project-config.ts
7376
7376
  var import_yaml = __toESM(require_dist(), 1);
7377
7377
 
7378
- // domains/comet-native/native-file-identity.ts
7378
+ // platform/fs/file-identity.ts
7379
7379
  function hasPlatformIdentity(value) {
7380
7380
  return value !== 0 && value !== 0n && value !== "0";
7381
7381
  }
7382
- function sameNativeFileObject(left, right) {
7382
+ function sameFileObject(left, right) {
7383
7383
  const comparableDevice = hasPlatformIdentity(left.dev) && hasPlatformIdentity(right.dev);
7384
7384
  if (comparableDevice && left.dev !== right.dev) return false;
7385
7385
  const comparableInode = hasPlatformIdentity(left.ino) && hasPlatformIdentity(right.ino);
@@ -7404,7 +7404,7 @@ function positiveLimit(value) {
7404
7404
  return value;
7405
7405
  }
7406
7406
  function sameDirectoryIdentity(expected, actual) {
7407
- return sameNativeFileObject(
7407
+ return sameFileObject(
7408
7408
  { ...expected, birthtime: expected.birthtimeMs },
7409
7409
  {
7410
7410
  ...actual,
@@ -7423,7 +7423,7 @@ function asFileIdentity(stat) {
7423
7423
  };
7424
7424
  }
7425
7425
  function sameFileIdentity(expected, actual) {
7426
- return sameNativeFileObject(
7426
+ return sameFileObject(
7427
7427
  { ...expected, birthtime: expected.birthtimeMs },
7428
7428
  {
7429
7429
  ...actual,
@@ -7625,11 +7625,100 @@ var NATIVE_KEYS = /* @__PURE__ */ new Set([
7625
7625
  "artifact_root",
7626
7626
  "language",
7627
7627
  "clarification_mode",
7628
+ "snapshot",
7628
7629
  "pending_root_move"
7629
7630
  ]);
7631
+ var SNAPSHOT_KEYS = /* @__PURE__ */ new Set([
7632
+ "include",
7633
+ "exclude",
7634
+ "max_files",
7635
+ "max_total_bytes",
7636
+ "max_duration_ms"
7637
+ ]);
7630
7638
  var PENDING_KEYS = /* @__PURE__ */ new Set(["id", "from_artifact_root", "to_artifact_root", "stage", "cleanup"]);
7631
7639
  var NATIVE_PROJECT_CONFIG_MAX_BYTES = 64 * 1024;
7632
7640
  var CLEANUP_KEYS = /* @__PURE__ */ new Set(["kind", "state", "manifest_hash"]);
7641
+ var MAX_NATIVE_SNAPSHOT_PATTERN_LENGTH = 1024;
7642
+ var MAX_NATIVE_SNAPSHOT_PATTERN_WILDCARDS = 64;
7643
+ var DEFAULT_NATIVE_SNAPSHOT_CONFIG = {
7644
+ include: ["**/*"],
7645
+ exclude: [],
7646
+ max_files: 1e4,
7647
+ max_total_bytes: 256 * 1024 * 1024,
7648
+ max_duration_ms: 6e4
7649
+ };
7650
+ function normalizeNativeSnapshotPattern(value, label) {
7651
+ if (typeof value !== "string" || value.length === 0 || value.includes("\\") || value.includes("\0") || value.startsWith("/") || value.split("/").includes("..")) {
7652
+ throw new Error(`${label} contains an unsafe pattern`);
7653
+ }
7654
+ if (value.length > MAX_NATIVE_SNAPSHOT_PATTERN_LENGTH) {
7655
+ throw new Error(`${label} exceeds ${MAX_NATIVE_SNAPSHOT_PATTERN_LENGTH} characters`);
7656
+ }
7657
+ let wildcardTokens = 0;
7658
+ for (let index = 0; index < value.length; index += 1) {
7659
+ if (value[index] === "?") {
7660
+ wildcardTokens += 1;
7661
+ } else if (value[index] === "*") {
7662
+ wildcardTokens += 1;
7663
+ if (value[index + 1] === "*") index += 1;
7664
+ }
7665
+ }
7666
+ if (wildcardTokens > MAX_NATIVE_SNAPSHOT_PATTERN_WILDCARDS) {
7667
+ throw new Error(
7668
+ `${label} contains more than ${MAX_NATIVE_SNAPSHOT_PATTERN_WILDCARDS} wildcard tokens`
7669
+ );
7670
+ }
7671
+ return value;
7672
+ }
7673
+ function snapshotPatterns(value, label, fallback) {
7674
+ if (value === void 0) return [...fallback];
7675
+ if (!Array.isArray(value)) {
7676
+ throw new Error(`${label} contains an unsafe pattern`);
7677
+ }
7678
+ return [...new Set(value.map((pattern) => normalizeNativeSnapshotPattern(pattern, label)))].sort(
7679
+ (left, right) => left.localeCompare(right, "en")
7680
+ );
7681
+ }
7682
+ function positiveSnapshotInteger(value, fallback, label) {
7683
+ const resolved = value ?? fallback;
7684
+ if (!Number.isSafeInteger(resolved) || resolved < 1) {
7685
+ throw new Error(`${label} must be a positive integer`);
7686
+ }
7687
+ return resolved;
7688
+ }
7689
+ function parseSnapshot(value) {
7690
+ if (value === void 0)
7691
+ return { ...DEFAULT_NATIVE_SNAPSHOT_CONFIG, include: ["**/*"], exclude: [] };
7692
+ const snapshot = record(value, "native.snapshot");
7693
+ rejectUnknown(snapshot, SNAPSHOT_KEYS, "native.snapshot");
7694
+ return {
7695
+ include: snapshotPatterns(
7696
+ snapshot.include,
7697
+ "native.snapshot.include",
7698
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.include
7699
+ ),
7700
+ exclude: snapshotPatterns(
7701
+ snapshot.exclude,
7702
+ "native.snapshot.exclude",
7703
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.exclude
7704
+ ),
7705
+ max_files: positiveSnapshotInteger(
7706
+ snapshot.max_files,
7707
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.max_files,
7708
+ "native.snapshot.max_files"
7709
+ ),
7710
+ max_total_bytes: positiveSnapshotInteger(
7711
+ snapshot.max_total_bytes,
7712
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.max_total_bytes,
7713
+ "native.snapshot.max_total_bytes"
7714
+ ),
7715
+ max_duration_ms: positiveSnapshotInteger(
7716
+ snapshot.max_duration_ms,
7717
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.max_duration_ms,
7718
+ "native.snapshot.max_duration_ms"
7719
+ )
7720
+ };
7721
+ }
7633
7722
  function record(value, label) {
7634
7723
  if (!value || typeof value !== "object" || Array.isArray(value)) {
7635
7724
  throw new Error(`${label} must be a mapping`);
@@ -7715,6 +7804,7 @@ function parseConfig(value) {
7715
7804
  throw new Error("native.clarification_mode must be sequential or batch");
7716
7805
  }
7717
7806
  const pending = parsePending(native.pending_root_move);
7807
+ const snapshot = parseSnapshot(native.snapshot);
7718
7808
  return {
7719
7809
  schema: "comet.project.v1",
7720
7810
  default_workflow: root.default_workflow,
@@ -7724,6 +7814,7 @@ function parseConfig(value) {
7724
7814
  artifact_root: normalizeArtifactRootRef(native.artifact_root),
7725
7815
  language,
7726
7816
  clarification_mode: clarificationMode,
7817
+ snapshot,
7727
7818
  ...pending ? { pending_root_move: pending } : {}
7728
7819
  }
7729
7820
  };
@@ -7964,8 +7964,127 @@ import { promises as fs6 } from "fs";
7964
7964
  import path6 from "path";
7965
7965
 
7966
7966
  // domains/comet-entry/current-selection.ts
7967
- import { promises as fs3 } from "fs";
7968
7967
  import path3 from "path";
7968
+
7969
+ // platform/fs/race-safe-read.ts
7970
+ import { constants as fsConstants, promises as fs3 } from "fs";
7971
+
7972
+ // platform/fs/file-identity.ts
7973
+ function hasPlatformIdentity(value) {
7974
+ return value !== 0 && value !== 0n && value !== "0";
7975
+ }
7976
+ function hasComparableFileObject(left, right) {
7977
+ return hasPlatformIdentity(left.dev) && hasPlatformIdentity(right.dev) && hasPlatformIdentity(left.ino) && hasPlatformIdentity(right.ino);
7978
+ }
7979
+ function sameFileObject(left, right) {
7980
+ const comparableDevice = hasPlatformIdentity(left.dev) && hasPlatformIdentity(right.dev);
7981
+ if (comparableDevice && left.dev !== right.dev) return false;
7982
+ const comparableInode = hasPlatformIdentity(left.ino) && hasPlatformIdentity(right.ino);
7983
+ if (comparableInode && left.ino !== right.ino) return false;
7984
+ if (comparableDevice && comparableInode) return true;
7985
+ return left.birthtime === right.birthtime;
7986
+ }
7987
+
7988
+ // platform/fs/race-safe-read.ts
7989
+ var RaceSafeReadError = class extends Error {
7990
+ reason;
7991
+ constructor(reason, message, options) {
7992
+ super(message, options);
7993
+ this.name = "RaceSafeReadError";
7994
+ this.reason = reason;
7995
+ }
7996
+ };
7997
+ function birthtimeOf(stat) {
7998
+ return "birthtimeNs" in stat && typeof stat.birthtimeNs === "bigint" ? stat.birthtimeNs : stat.birthtimeMs;
7999
+ }
8000
+ function ctimeOf(stat) {
8001
+ return "ctimeNs" in stat && typeof stat.ctimeNs === "bigint" ? stat.ctimeNs : stat.ctimeMs;
8002
+ }
8003
+ function identityOf(stat) {
8004
+ return { dev: stat.dev, ino: stat.ino, birthtime: birthtimeOf(stat) };
8005
+ }
8006
+ function sameStatIdentity(left, right) {
8007
+ const leftObject = identityOf(left);
8008
+ const rightObject = identityOf(right);
8009
+ if (hasComparableFileObject(leftObject, rightObject)) {
8010
+ return sameFileObject(leftObject, rightObject);
8011
+ }
8012
+ return sameFileObject(leftObject, rightObject) && birthtimeOf(left) === birthtimeOf(right) && ctimeOf(left) === ctimeOf(right) && left.size === right.size;
8013
+ }
8014
+ async function readFileRaceSafe(file, maxBytes, options = {}) {
8015
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
8016
+ throw new Error("race-safe read byte limit must be a positive integer");
8017
+ }
8018
+ const label = options.label ?? "file";
8019
+ const bigint = options.bigint === true;
8020
+ const before = await fs3.lstat(file, { bigint });
8021
+ if (!before.isFile() || before.isSymbolicLink()) {
8022
+ throw new RaceSafeReadError("not-regular-file", `${label} must be a regular file`);
8023
+ }
8024
+ if (BigInt(before.size) > BigInt(maxBytes)) {
8025
+ throw new RaceSafeReadError("too-large", `${label} exceeds ${maxBytes} bytes`);
8026
+ }
8027
+ const beforeRealPath = await fs3.realpath(file);
8028
+ await options.verify?.("pre-open", { realPath: beforeRealPath, identity: identityOf(before) });
8029
+ const flags = process.platform === "win32" ? fsConstants.O_RDONLY : fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK;
8030
+ let handle;
8031
+ try {
8032
+ handle = await fs3.open(file, flags);
8033
+ } catch (error) {
8034
+ if (error.code === "ELOOP") {
8035
+ throw new RaceSafeReadError("not-regular-file", `${label} must be a regular file`, {
8036
+ cause: error
8037
+ });
8038
+ }
8039
+ throw error;
8040
+ }
8041
+ try {
8042
+ const [opened, pathAfterOpen, realPathAfterOpen] = await Promise.all([
8043
+ handle.stat({ bigint }),
8044
+ fs3.lstat(file, { bigint }),
8045
+ fs3.realpath(file)
8046
+ ]);
8047
+ if (!opened.isFile() || !pathAfterOpen.isFile() || pathAfterOpen.isSymbolicLink() || realPathAfterOpen !== beforeRealPath || !sameStatIdentity(before, opened) || !sameStatIdentity(before, pathAfterOpen)) {
8048
+ throw new RaceSafeReadError("changed", `${label} changed while opening`);
8049
+ }
8050
+ await options.verify?.("post-open", {
8051
+ realPath: realPathAfterOpen,
8052
+ identity: identityOf(opened)
8053
+ });
8054
+ await options.hooks?.afterOpen?.();
8055
+ const chunks = [];
8056
+ let total = 0;
8057
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1));
8058
+ for (; ; ) {
8059
+ const remaining = maxBytes + 1 - total;
8060
+ const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, remaining), null);
8061
+ if (bytesRead === 0) break;
8062
+ total += bytesRead;
8063
+ if (total > maxBytes) {
8064
+ throw new RaceSafeReadError("too-large", `${label} exceeds ${maxBytes} bytes`);
8065
+ }
8066
+ chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
8067
+ }
8068
+ await options.hooks?.beforeFinalCheck?.();
8069
+ const [afterHandle, afterPath, afterRealPath] = await Promise.all([
8070
+ handle.stat({ bigint }),
8071
+ fs3.lstat(file, { bigint }),
8072
+ fs3.realpath(file)
8073
+ ]);
8074
+ if (!afterPath.isFile() || afterPath.isSymbolicLink() || afterRealPath !== beforeRealPath || !sameStatIdentity(before, afterHandle) || !sameStatIdentity(before, afterPath)) {
8075
+ throw new RaceSafeReadError("changed", `${label} changed while reading`);
8076
+ }
8077
+ await options.verify?.("post-read", {
8078
+ realPath: afterRealPath,
8079
+ identity: identityOf(afterHandle)
8080
+ });
8081
+ return { bytes: Buffer.concat(chunks, total), stat: afterHandle, realPath: afterRealPath };
8082
+ } finally {
8083
+ await handle.close();
8084
+ }
8085
+ }
8086
+
8087
+ // domains/comet-entry/current-selection.ts
7969
8088
  var COMET_CURRENT_SELECTION_SCHEMA = "comet.selection.v2";
7970
8089
  var COMET_CURRENT_SELECTION_MAX_BYTES = 16 * 1024;
7971
8090
  function cometCurrentSelectionFile(projectRoot) {
@@ -8027,16 +8146,11 @@ function parseSelection(source) {
8027
8146
  async function readCometCurrentSelection(projectRoot) {
8028
8147
  let source;
8029
8148
  try {
8030
- const stat = await fs3.lstat(cometCurrentSelectionFile(projectRoot));
8031
- if (stat.isSymbolicLink() || !stat.isFile()) {
8032
- throw new Error("current change selection must be a regular file");
8033
- }
8034
- if (stat.size > COMET_CURRENT_SELECTION_MAX_BYTES) {
8035
- throw new Error(
8036
- `current change selection exceeds ${COMET_CURRENT_SELECTION_MAX_BYTES} bytes`
8037
- );
8038
- }
8039
- source = await fs3.readFile(cometCurrentSelectionFile(projectRoot), "utf8");
8149
+ const file = cometCurrentSelectionFile(projectRoot);
8150
+ const { bytes } = await readFileRaceSafe(file, COMET_CURRENT_SELECTION_MAX_BYTES, {
8151
+ label: "current change selection"
8152
+ });
8153
+ source = bytes.toString("utf8");
8040
8154
  } catch (error) {
8041
8155
  if (error.code === "ENOENT") return { status: "missing" };
8042
8156
  throw new Error(
@@ -9054,22 +9168,6 @@ function nativeSensitiveRelativePathReason(relativeRef) {
9054
9168
  return null;
9055
9169
  }
9056
9170
 
9057
- // domains/comet-native/native-file-identity.ts
9058
- function hasPlatformIdentity(value) {
9059
- return value !== 0 && value !== 0n && value !== "0";
9060
- }
9061
- function hasComparableNativeFileObject(left, right) {
9062
- return hasPlatformIdentity(left.dev) && hasPlatformIdentity(right.dev) && hasPlatformIdentity(left.ino) && hasPlatformIdentity(right.ino);
9063
- }
9064
- function sameNativeFileObject(left, right) {
9065
- const comparableDevice = hasPlatformIdentity(left.dev) && hasPlatformIdentity(right.dev);
9066
- if (comparableDevice && left.dev !== right.dev) return false;
9067
- const comparableInode = hasPlatformIdentity(left.ino) && hasPlatformIdentity(right.ino);
9068
- if (comparableInode && left.ino !== right.ino) return false;
9069
- if (comparableDevice && comparableInode) return true;
9070
- return left.birthtime === right.birthtime;
9071
- }
9072
-
9073
9171
  // domains/comet-native/native-bounded-file.ts
9074
9172
  var DEFAULT_NATIVE_ARTIFACT_MAX_BYTES = 1024 * 1024;
9075
9173
  function isInside(parent, target) {
@@ -9100,7 +9198,7 @@ function positiveLimit(value) {
9100
9198
  return value;
9101
9199
  }
9102
9200
  function sameDirectoryIdentity(identity, stat) {
9103
- return sameNativeFileObject(
9201
+ return sameFileObject(
9104
9202
  { ...identity, birthtime: identity.birthtimeMs },
9105
9203
  {
9106
9204
  ...stat,
@@ -9111,10 +9209,10 @@ function sameDirectoryIdentity(identity, stat) {
9111
9209
  function sameFileIdentity(left, right) {
9112
9210
  const leftObject = { ...left, birthtime: left.birthtimeMs };
9113
9211
  const rightObject = { ...right, birthtime: right.birthtimeMs };
9114
- if (hasComparableNativeFileObject(leftObject, rightObject)) {
9115
- return sameNativeFileObject(leftObject, rightObject);
9212
+ if (hasComparableFileObject(leftObject, rightObject)) {
9213
+ return sameFileObject(leftObject, rightObject);
9116
9214
  }
9117
- return sameNativeFileObject(leftObject, rightObject) && left.birthtimeMs === right.birthtimeMs && left.ctimeMs === right.ctimeMs && left.size === right.size;
9215
+ return sameFileObject(leftObject, rightObject) && left.birthtimeMs === right.birthtimeMs && left.ctimeMs === right.ctimeMs && left.size === right.size;
9118
9216
  }
9119
9217
  async function directoryIdentity(directory) {
9120
9218
  const stat = await fs8.lstat(directory);
@@ -9231,7 +9329,7 @@ var import_yaml3 = __toESM(require_dist(), 1);
9231
9329
 
9232
9330
  // domains/comet-native/native-protected-file.ts
9233
9331
  import { createHash as createHash2 } from "node:crypto";
9234
- import { constants as fsConstants, promises as fs9 } from "node:fs";
9332
+ import { constants as fsConstants2, promises as fs9 } from "node:fs";
9235
9333
  import path9 from "node:path";
9236
9334
  import { TextDecoder as TextDecoder2 } from "node:util";
9237
9335
  function isInside2(parent, target) {
@@ -9245,7 +9343,7 @@ function positiveLimit2(value) {
9245
9343
  return value;
9246
9344
  }
9247
9345
  function sameDirectoryIdentity2(expected, actual) {
9248
- return sameNativeFileObject(
9346
+ return sameFileObject(
9249
9347
  { ...expected, birthtime: expected.birthtimeMs },
9250
9348
  {
9251
9349
  ...actual,
@@ -9264,7 +9362,7 @@ function asFileIdentity(stat) {
9264
9362
  };
9265
9363
  }
9266
9364
  function sameFileIdentity2(expected, actual) {
9267
- return sameNativeFileObject(
9365
+ return sameFileObject(
9268
9366
  { ...expected, birthtime: expected.birthtimeMs },
9269
9367
  {
9270
9368
  ...actual,
@@ -9351,7 +9449,7 @@ async function readNativeProtectedFile(options) {
9351
9449
  if (forbidden.some((identity) => isInside2(identity.realPath, beforeRealPath))) {
9352
9450
  throw new Error(`${options.label} resolves inside an excluded root`);
9353
9451
  }
9354
- const flags = process.platform === "win32" ? fsConstants.O_RDONLY : fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK;
9452
+ const flags = process.platform === "win32" ? fsConstants2.O_RDONLY : fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW | fsConstants2.O_NONBLOCK;
9355
9453
  const handle = await fs9.open(file, flags);
9356
9454
  try {
9357
9455
  const opened = await handle.stat();
@@ -9403,11 +9501,100 @@ var NATIVE_KEYS = /* @__PURE__ */ new Set([
9403
9501
  "artifact_root",
9404
9502
  "language",
9405
9503
  "clarification_mode",
9504
+ "snapshot",
9406
9505
  "pending_root_move"
9407
9506
  ]);
9507
+ var SNAPSHOT_KEYS = /* @__PURE__ */ new Set([
9508
+ "include",
9509
+ "exclude",
9510
+ "max_files",
9511
+ "max_total_bytes",
9512
+ "max_duration_ms"
9513
+ ]);
9408
9514
  var PENDING_KEYS = /* @__PURE__ */ new Set(["id", "from_artifact_root", "to_artifact_root", "stage", "cleanup"]);
9409
9515
  var NATIVE_PROJECT_CONFIG_MAX_BYTES = 64 * 1024;
9410
9516
  var CLEANUP_KEYS = /* @__PURE__ */ new Set(["kind", "state", "manifest_hash"]);
9517
+ var MAX_NATIVE_SNAPSHOT_PATTERN_LENGTH = 1024;
9518
+ var MAX_NATIVE_SNAPSHOT_PATTERN_WILDCARDS = 64;
9519
+ var DEFAULT_NATIVE_SNAPSHOT_CONFIG = {
9520
+ include: ["**/*"],
9521
+ exclude: [],
9522
+ max_files: 1e4,
9523
+ max_total_bytes: 256 * 1024 * 1024,
9524
+ max_duration_ms: 6e4
9525
+ };
9526
+ function normalizeNativeSnapshotPattern(value, label) {
9527
+ if (typeof value !== "string" || value.length === 0 || value.includes("\\") || value.includes("\0") || value.startsWith("/") || value.split("/").includes("..")) {
9528
+ throw new Error(`${label} contains an unsafe pattern`);
9529
+ }
9530
+ if (value.length > MAX_NATIVE_SNAPSHOT_PATTERN_LENGTH) {
9531
+ throw new Error(`${label} exceeds ${MAX_NATIVE_SNAPSHOT_PATTERN_LENGTH} characters`);
9532
+ }
9533
+ let wildcardTokens = 0;
9534
+ for (let index = 0; index < value.length; index += 1) {
9535
+ if (value[index] === "?") {
9536
+ wildcardTokens += 1;
9537
+ } else if (value[index] === "*") {
9538
+ wildcardTokens += 1;
9539
+ if (value[index + 1] === "*") index += 1;
9540
+ }
9541
+ }
9542
+ if (wildcardTokens > MAX_NATIVE_SNAPSHOT_PATTERN_WILDCARDS) {
9543
+ throw new Error(
9544
+ `${label} contains more than ${MAX_NATIVE_SNAPSHOT_PATTERN_WILDCARDS} wildcard tokens`
9545
+ );
9546
+ }
9547
+ return value;
9548
+ }
9549
+ function snapshotPatterns(value, label, fallback) {
9550
+ if (value === void 0) return [...fallback];
9551
+ if (!Array.isArray(value)) {
9552
+ throw new Error(`${label} contains an unsafe pattern`);
9553
+ }
9554
+ return [...new Set(value.map((pattern) => normalizeNativeSnapshotPattern(pattern, label)))].sort(
9555
+ (left, right) => left.localeCompare(right, "en")
9556
+ );
9557
+ }
9558
+ function positiveSnapshotInteger(value, fallback, label) {
9559
+ const resolved = value ?? fallback;
9560
+ if (!Number.isSafeInteger(resolved) || resolved < 1) {
9561
+ throw new Error(`${label} must be a positive integer`);
9562
+ }
9563
+ return resolved;
9564
+ }
9565
+ function parseSnapshot(value) {
9566
+ if (value === void 0)
9567
+ return { ...DEFAULT_NATIVE_SNAPSHOT_CONFIG, include: ["**/*"], exclude: [] };
9568
+ const snapshot = record(value, "native.snapshot");
9569
+ rejectUnknown(snapshot, SNAPSHOT_KEYS, "native.snapshot");
9570
+ return {
9571
+ include: snapshotPatterns(
9572
+ snapshot.include,
9573
+ "native.snapshot.include",
9574
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.include
9575
+ ),
9576
+ exclude: snapshotPatterns(
9577
+ snapshot.exclude,
9578
+ "native.snapshot.exclude",
9579
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.exclude
9580
+ ),
9581
+ max_files: positiveSnapshotInteger(
9582
+ snapshot.max_files,
9583
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.max_files,
9584
+ "native.snapshot.max_files"
9585
+ ),
9586
+ max_total_bytes: positiveSnapshotInteger(
9587
+ snapshot.max_total_bytes,
9588
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.max_total_bytes,
9589
+ "native.snapshot.max_total_bytes"
9590
+ ),
9591
+ max_duration_ms: positiveSnapshotInteger(
9592
+ snapshot.max_duration_ms,
9593
+ DEFAULT_NATIVE_SNAPSHOT_CONFIG.max_duration_ms,
9594
+ "native.snapshot.max_duration_ms"
9595
+ )
9596
+ };
9597
+ }
9411
9598
  function record(value, label) {
9412
9599
  if (!value || typeof value !== "object" || Array.isArray(value)) {
9413
9600
  throw new Error(`${label} must be a mapping`);
@@ -9493,6 +9680,7 @@ function parseConfig(value) {
9493
9680
  throw new Error("native.clarification_mode must be sequential or batch");
9494
9681
  }
9495
9682
  const pending = parsePending(native.pending_root_move);
9683
+ const snapshot = parseSnapshot(native.snapshot);
9496
9684
  return {
9497
9685
  schema: "comet.project.v1",
9498
9686
  default_workflow: root.default_workflow,
@@ -9502,6 +9690,7 @@ function parseConfig(value) {
9502
9690
  artifact_root: normalizeArtifactRootRef(native.artifact_root),
9503
9691
  language,
9504
9692
  clarification_mode: clarificationMode,
9693
+ snapshot,
9505
9694
  ...pending ? { pending_root_move: pending } : {}
9506
9695
  }
9507
9696
  };