snipara-companion 4.2.1 → 4.2.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Release notes for `snipara-companion`, newest first.
4
4
 
5
+ ## 4.2.2
6
+
7
+ - Propagate collaboration guard failures and run the pre-commit guard before mutating staged files.
8
+
5
9
  ## 4.2.1
6
10
 
7
11
  - Forward validation scope only to Sandbox commands that accept it, while preserving scoped coordinator runs.
package/dist/index.js CHANGED
@@ -24106,9 +24106,11 @@ function loadCollaborationState(rootDir = process.cwd()) {
24106
24106
  schemaVersion: "snipara.collaboration.v1",
24107
24107
  updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : (/* @__PURE__ */ new Date()).toISOString(),
24108
24108
  workSessionId: normalizeOptionalString3(parsed.workSessionId),
24109
- workflowSessions: Object.fromEntries(Object.entries(parsed.workflowSessions ?? {}).filter(
24110
- ([key, value]) => /^[a-f0-9]{64}$/.test(key) && typeof value === "string" && value.length > 0
24111
- )),
24109
+ workflowSessions: Object.fromEntries(
24110
+ Object.entries(parsed.workflowSessions ?? {}).filter(
24111
+ ([key, value]) => /^[a-f0-9]{64}$/.test(key) && typeof value === "string" && value.length > 0
24112
+ )
24113
+ ),
24112
24114
  sessionId: normalizeOptionalString3(parsed.sessionId),
24113
24115
  actorId: normalizeOptionalString3(parsed.actorId),
24114
24116
  actorType: normalizeActorType(parsed.actorType),
@@ -24357,10 +24359,15 @@ async function workflowCollaborationStart(options) {
24357
24359
  const session = hostedSession.status === "ok" ? hostedSession.data?.session : void 0;
24358
24360
  const workSessionId = session?.id ?? persistedSessionId ?? logicalSessionId;
24359
24361
  if (session) {
24360
- state.workflowSessions = { ...state.workflowSessions, [sessionKey]: session.id };
24362
+ state.workflowSessions = {
24363
+ ...state.workflowSessions,
24364
+ [sessionKey]: session.id
24365
+ };
24361
24366
  }
24362
24367
  const effectiveResources = hostedSession.status === "ok" ? hostedSession.data?.resources ?? resources : resources;
24363
- let hostedClaim = { status: "skipped" };
24368
+ let hostedClaim = {
24369
+ status: "skipped"
24370
+ };
24364
24371
  const shouldClaim = options.mode === "full" || options.mode === "orchestrate";
24365
24372
  if (hostedSession.status === "ok" && shouldClaim && (files.length > 0 || effectiveResources.length > 0)) {
24366
24373
  hostedClaim = await maybeCreateHostedLeases(context, {
@@ -24895,7 +24902,9 @@ async function runCollaborationWatchTick(context, state, options) {
24895
24902
  let heartbeatHosted = {
24896
24903
  status: "skipped"
24897
24904
  };
24898
- let claimHosted = { status: "skipped" };
24905
+ let claimHosted = {
24906
+ status: "skipped"
24907
+ };
24899
24908
  let releaseHosted = {
24900
24909
  status: "skipped"
24901
24910
  };
@@ -24942,7 +24951,9 @@ async function runCollaborationWatchTick(context, state, options) {
24942
24951
  releaseHosted = await releaseHostedLeases(
24943
24952
  context,
24944
24953
  staleActiveLeases.map((lease) => lease.id),
24945
- { reason: "Released by collaboration watch because the resource is no longer active." }
24954
+ {
24955
+ reason: "Released by collaboration watch because the resource is no longer active."
24956
+ }
24946
24957
  );
24947
24958
  const releasedIds = new Set(
24948
24959
  releaseHosted.status === "ok" ? (releaseHosted.data ?? []).map((item) => item.lease.id) : staleActiveLeases.map((lease) => lease.id)
@@ -25044,30 +25055,58 @@ function deriveGuardProfileResources(profile, files) {
25044
25055
  }
25045
25056
  if (profile === "pre-deploy") {
25046
25057
  resources.push(
25047
- { kind: "DEPLOY", id: "production-deployment", label: "Production deployment" },
25048
- { kind: "SURFACE", id: "deployment", label: "Deployment and infrastructure" }
25058
+ {
25059
+ kind: "DEPLOY",
25060
+ id: "production-deployment",
25061
+ label: "Production deployment"
25062
+ },
25063
+ {
25064
+ kind: "SURFACE",
25065
+ id: "deployment",
25066
+ label: "Deployment and infrastructure"
25067
+ }
25049
25068
  );
25050
25069
  }
25051
25070
  if (profile === "migration" || profile === "schema") {
25052
25071
  resources.push(
25053
- { kind: "SCHEMA", id: "prisma:tenant_snipara", label: "Prisma tenant_snipara schema" },
25072
+ {
25073
+ kind: "SCHEMA",
25074
+ id: "prisma:tenant_snipara",
25075
+ label: "Prisma tenant_snipara schema"
25076
+ },
25054
25077
  { kind: "SURFACE", id: "database", label: "Database and schema" }
25055
25078
  );
25056
25079
  }
25057
25080
  if (profile === "release-package") {
25058
- resources.push({ kind: "SURFACE", id: "release-package", label: "Package release surface" });
25081
+ resources.push({
25082
+ kind: "SURFACE",
25083
+ id: "release-package",
25084
+ label: "Package release surface"
25085
+ });
25059
25086
  }
25060
25087
  if (profile === "pre-commit" || profile === "pre-push") {
25061
25088
  if (files.some(isPrismaSchemaPath)) {
25062
25089
  resources.push(
25063
- { kind: "SCHEMA", id: "prisma:tenant_snipara", label: "Prisma tenant_snipara schema" },
25090
+ {
25091
+ kind: "SCHEMA",
25092
+ id: "prisma:tenant_snipara",
25093
+ label: "Prisma tenant_snipara schema"
25094
+ },
25064
25095
  { kind: "SURFACE", id: "database", label: "Database and schema" }
25065
25096
  );
25066
25097
  }
25067
25098
  if (files.some(isDeploymentPath)) {
25068
25099
  resources.push(
25069
- { kind: "DEPLOY", id: "production-deployment", label: "Production deployment" },
25070
- { kind: "SURFACE", id: "deployment", label: "Deployment and infrastructure" }
25100
+ {
25101
+ kind: "DEPLOY",
25102
+ id: "production-deployment",
25103
+ label: "Production deployment"
25104
+ },
25105
+ {
25106
+ kind: "SURFACE",
25107
+ id: "deployment",
25108
+ label: "Deployment and infrastructure"
25109
+ }
25071
25110
  );
25072
25111
  }
25073
25112
  if (files.some((file) => file.endsWith("package.json") || file.includes("/package.json"))) {
@@ -25256,7 +25295,7 @@ function buildCollaborationGitHookBlock(hookName) {
25256
25295
  const profile = hookName === "pre-push" ? "pre-push" : "pre-commit";
25257
25296
  return [
25258
25297
  collaborationHookBlockMarker(hookName, "start"),
25259
- "# Atomically claim checkout git writes, then block unsafe parallel edits.",
25298
+ profile === "pre-commit" ? "# Request a cooperative checkout lease before changing the index." : "# Atomically claim checkout git writes, then block unsafe parallel edits.",
25260
25299
  'if [ "${SNIPARA_COLLABORATION_GUARD:-1}" = "0" ]; then',
25261
25300
  ' echo "Snipara collaboration guard bypassed by SNIPARA_COLLABORATION_GUARD=0" >&2',
25262
25301
  "else",
@@ -25270,7 +25309,7 @@ function buildCollaborationGitHookBlock(hookName) {
25270
25309
  " return 127",
25271
25310
  " fi",
25272
25311
  " }",
25273
- ` snipara_companion_hook collaboration claim --resource SURFACE:checkout-git-write --mode EXCLUSIVE --ttl-seconds 300 --reason "${profile} git write" --json >/dev/null || true`,
25312
+ ` snipara_companion_hook collaboration claim --resource SURFACE:checkout-git-write --mode EXCLUSIVE --ttl-seconds 300 --reason "${profile} git write" --json >/dev/null || ${profile === "pre-commit" ? "exit $?" : "true"}`,
25274
25313
  " snipara_companion_guard() {",
25275
25314
  ` snipara_companion_hook collaboration guard --profile ${profile} --action ${profile} --resource SURFACE:checkout-git-write --enforce "$@" --json >/dev/null`,
25276
25315
  " }",
@@ -25287,7 +25326,7 @@ function buildCollaborationGitHookBlock(hookName) {
25287
25326
  " else",
25288
25327
  " snipara_companion_guard",
25289
25328
  " fi",
25290
- "fi",
25329
+ profile === "pre-commit" ? "fi || exit $?" : "fi",
25291
25330
  collaborationHookBlockMarker(hookName, "end")
25292
25331
  ].join("\n");
25293
25332
  }
@@ -25303,6 +25342,15 @@ function mergeManagedCollaborationHookBlock(hookName, currentContent, block) {
25303
25342
  ${base}`;
25304
25343
  const nextBlock = `${block}
25305
25344
  `;
25345
+ if (hookName === "pre-commit") {
25346
+ const newline = withShebang.indexOf("\n");
25347
+ const shebang = newline < 0 ? withShebang : withShebang.slice(0, newline);
25348
+ const body = newline < 0 ? "" : withShebang.slice(newline + 1);
25349
+ return ensureTrailingNewline2(
25350
+ `${shebang}
25351
+ ${nextBlock}${body.replace(managedBlockPattern, "")}`
25352
+ );
25353
+ }
25306
25354
  if (managedBlockPattern.test(withShebang)) {
25307
25355
  return ensureTrailingNewline2(withShebang.replace(managedBlockPattern, nextBlock));
25308
25356
  }
@@ -25388,7 +25436,16 @@ function expireLocalLeases(state, now = /* @__PURE__ */ new Date()) {
25388
25436
  }
25389
25437
  }
25390
25438
  function workflowSessionKey(context, workflowId) {
25391
- return (0, import_node_crypto17.createHash)("sha256").update(JSON.stringify([context.config.apiUrl, context.config.projectId, context.actor.actorType, context.actor.actorId, context.worktree, workflowId])).digest("hex");
25439
+ return (0, import_node_crypto17.createHash)("sha256").update(
25440
+ JSON.stringify([
25441
+ context.config.apiUrl,
25442
+ context.config.projectId,
25443
+ context.actor.actorType,
25444
+ context.actor.actorId,
25445
+ context.worktree,
25446
+ workflowId
25447
+ ])
25448
+ ).digest("hex");
25392
25449
  }
25393
25450
  function workflowWorkSessionId(workflowId) {
25394
25451
  return `workflow:${workflowId}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "4.2.1",
3
+ "version": "4.2.2",
4
4
  "description": "Local-first CLI that asks your repo what breaks before an AI coding agent edits it.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {