snipara-companion 3.2.29 → 3.2.30

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,17 @@
2
2
 
3
3
  Release notes for `snipara-companion`, newest first.
4
4
 
5
+ ## New In 3.2.30
6
+
7
+ - Persists an explicit `--ack-review-only` as a 15-minute, one-use
8
+ acknowledgement bound to the exact guard profile, action, and finding
9
+ fingerprint, so the immediately following Git hook can consume it safely.
10
+ - Invalidates that acknowledgement when findings change and never applies it
11
+ to required acknowledgements, active conflicts, or hard blocks.
12
+ - Resolves collaboration state from `SNIPARA_WORKSPACE_DIR` or the Git
13
+ top-level directory, preventing filtered package commands from writing a
14
+ nested `packages/cli/.snipara/` session.
15
+
5
16
  ## New In 3.2.29
6
17
 
7
18
  - Extends `workflow producer-report` with attributed gated-execution samples
package/dist/index.d.ts CHANGED
@@ -2831,6 +2831,7 @@ declare function buildCodingIntelligenceLedger(options?: CodingLedgerBuildOption
2831
2831
  declare function codingLedgerExportCommand(options: CodingLedgerExportCommandOptions): Promise<void>;
2832
2832
 
2833
2833
  declare const COLLABORATION_STATE_RELATIVE_PATH: string;
2834
+ type CollaborationGuardProfile = "edit" | "pre-commit" | "pre-push" | "pre-deploy" | "migration" | "schema" | "release-package";
2834
2835
  type CollaborationGuardActionKind = "safe_to_ack" | "needs_handoff" | "needs_test" | "needs_package_review" | "blocking_conflict" | "guard_unavailable";
2835
2836
  interface CollaborationGuardActionCard {
2836
2837
  kind: CollaborationGuardActionKind;
@@ -2910,6 +2911,13 @@ interface CollaborationLocalLeaseRecord {
2910
2911
  expiresAt?: string | null;
2911
2912
  workSessionId?: string;
2912
2913
  }
2914
+ interface CollaborationReviewOnlyAcknowledgement {
2915
+ profile: CollaborationGuardProfile;
2916
+ action: string;
2917
+ findingFingerprint: string;
2918
+ acknowledgedAt: string;
2919
+ expiresAt: string;
2920
+ }
2913
2921
  interface CollaborationLocalState {
2914
2922
  schemaVersion: "snipara.collaboration.v1";
2915
2923
  updatedAt: string;
@@ -2926,6 +2934,7 @@ interface CollaborationLocalState {
2926
2934
  files: string[];
2927
2935
  resources: CollaborationResource[];
2928
2936
  leases: CollaborationLocalLeaseRecord[];
2937
+ reviewOnlyAcknowledgements: CollaborationReviewOnlyAcknowledgement[];
2929
2938
  lastGuard?: {
2930
2939
  decision: CollaborationGuardDecision;
2931
2940
  severity: string;
package/dist/index.js CHANGED
@@ -15400,6 +15400,7 @@ var DEFAULT_LOCAL_CODE_MAX_FILES = 2e3;
15400
15400
  var HOSTED_GUARD_MAX_FILES = 450;
15401
15401
  var HOSTED_GUARD_MAX_RESOURCES = 850;
15402
15402
  var HOSTED_GUARD_MAX_SYMBOL_RESOURCES = 160;
15403
+ var REVIEW_ONLY_ACK_TTL_MS = 15 * 60 * 1e3;
15403
15404
  var HOOK_BLOCK_PREFIX2 = "snipara:collaboration-guard";
15404
15405
  var CHECKOUT_GIT_WRITE_RESOURCE = {
15405
15406
  kind: "SURFACE",
@@ -15460,7 +15461,8 @@ function createEmptyCollaborationState(now = /* @__PURE__ */ new Date()) {
15460
15461
  updatedAt: now.toISOString(),
15461
15462
  files: [],
15462
15463
  resources: [],
15463
- leases: []
15464
+ leases: [],
15465
+ reviewOnlyAcknowledgements: []
15464
15466
  };
15465
15467
  }
15466
15468
  function getCollaborationStatePath(rootDir = process.cwd()) {
@@ -15493,13 +15495,18 @@ function loadCollaborationState(rootDir = process.cwd()) {
15493
15495
  files: normalizeFiles(parsed.files),
15494
15496
  resources: normalizeResources(parsed.resources),
15495
15497
  leases: normalizeLocalLeases(parsed.leases),
15498
+ reviewOnlyAcknowledgements: normalizeReviewOnlyAcknowledgements(
15499
+ parsed.reviewOnlyAcknowledgements
15500
+ ),
15496
15501
  lastGuard: normalizeLastGuard(parsed.lastGuard)
15497
15502
  };
15498
15503
  expireLocalLeases(state);
15504
+ expireReviewOnlyAcknowledgements(state);
15499
15505
  return state;
15500
15506
  }
15501
15507
  function saveCollaborationState(state, rootDir = process.cwd()) {
15502
15508
  expireLocalLeases(state);
15509
+ expireReviewOnlyAcknowledgements(state);
15503
15510
  const statePath = getCollaborationStatePath(rootDir);
15504
15511
  fs20.mkdirSync(path19.dirname(statePath), { recursive: true });
15505
15512
  const tempPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
@@ -15814,13 +15821,14 @@ async function collaborationGuardCommand(options) {
15814
15821
  const context = resolveCollaborationContext(options);
15815
15822
  const state = loadCollaborationState(context.rootDir);
15816
15823
  const profile = normalizeGuardProfile(options.profile) ?? "edit";
15824
+ const action = normalizeOptionalString3(options.action) ?? profile;
15817
15825
  const files = resolveGuardFiles(context, options, profile);
15818
15826
  const resources = resolveCommandResources(context, files, options, profile);
15819
15827
  ensureFilesOrResources(files, resources, "guard");
15820
15828
  const hostedPayload = buildHostedGuardPayload(files, resources);
15821
15829
  const hosted = await maybeEvaluateHostedGuard(context, {
15822
15830
  workSessionId: options.workSessionId ?? state.workSessionId,
15823
- action: normalizeOptionalString3(options.action) ?? profile,
15831
+ action,
15824
15832
  files: hostedPayload.files,
15825
15833
  resources: hostedPayload.resources,
15826
15834
  persist: options.persist !== false
@@ -15832,26 +15840,39 @@ async function collaborationGuardCommand(options) {
15832
15840
  decision: evaluation.decision,
15833
15841
  severity: evaluation.severity,
15834
15842
  checkedAt: evaluation.evaluatedAt,
15835
- action: normalizeOptionalString3(options.action) ?? profile,
15843
+ action,
15836
15844
  resources: evaluation.resources,
15837
15845
  conflictCount: evaluation.conflicts.length
15838
15846
  };
15839
15847
  state.files = mergeStrings(state.files, files);
15840
15848
  state.resources = mergeResources(state.resources, evaluation.resources);
15849
+ }
15850
+ const explicitReviewOnlyAck = Boolean(options.enforce) && Boolean(options.ackReviewOnly) && isReviewOnlyGuardEvaluation(evaluation);
15851
+ const findingFingerprint = evaluation && isReviewOnlyGuardEvaluation(evaluation) ? fingerprintReviewOnlyGuardEvaluation(evaluation) : void 0;
15852
+ const persistedReviewOnlyAck = Boolean(options.enforce) && !options.ackReviewOnly && Boolean(
15853
+ findingFingerprint && consumeReviewOnlyAcknowledgement(state, profile, action, findingFingerprint)
15854
+ );
15855
+ if (explicitReviewOnlyAck && findingFingerprint) {
15856
+ persistReviewOnlyAcknowledgement(state, profile, action, findingFingerprint);
15857
+ } else if (evaluation && !persistedReviewOnlyAck) {
15858
+ invalidateReviewOnlyAcknowledgements(state, profile, action);
15859
+ }
15860
+ const reviewOnlyAcknowledged = explicitReviewOnlyAck || persistedReviewOnlyAck;
15861
+ const ackSource = explicitReviewOnlyAck ? "explicit" : persistedReviewOnlyAck ? "persisted" : "none";
15862
+ if (evaluation) {
15841
15863
  saveCollaborationState(state, context.rootDir);
15842
15864
  }
15843
- const reviewOnlyAcknowledged = Boolean(options.enforce) && Boolean(options.ackReviewOnly) && isReviewOnlyGuardEvaluation(evaluation);
15844
15865
  const actionCards = buildCollaborationGuardActionCards(evaluation, {
15845
15866
  profile,
15846
15867
  enforced: Boolean(options.enforce),
15847
- ackReviewOnly: Boolean(options.ackReviewOnly),
15868
+ ackReviewOnly: reviewOnlyAcknowledged,
15848
15869
  reviewOnlyAcknowledged
15849
15870
  });
15850
15871
  const guardFailed = shouldFailCollaborationGuard({
15851
15872
  hostedStatus: hosted.status,
15852
15873
  evaluation,
15853
15874
  enforce: Boolean(options.enforce),
15854
- ackReviewOnly: Boolean(options.ackReviewOnly)
15875
+ ackReviewOnly: reviewOnlyAcknowledged
15855
15876
  });
15856
15877
  if (guardFailed) {
15857
15878
  process.exitCode = 2;
@@ -15867,6 +15888,7 @@ async function collaborationGuardCommand(options) {
15867
15888
  enforced: Boolean(options.enforce),
15868
15889
  ackReviewOnly: Boolean(options.ackReviewOnly),
15869
15890
  reviewOnlyAcknowledged,
15891
+ ackSource,
15870
15892
  failed: guardFailed
15871
15893
  }
15872
15894
  },
@@ -16036,7 +16058,8 @@ async function collaborationIdeStatusCommand(options) {
16036
16058
  console.log(`Active leases: ${payload.hosted.activeLeases.length}`);
16037
16059
  }
16038
16060
  function resolveCollaborationContext(options) {
16039
- const rootDir = path19.resolve(options.dir ?? process.cwd());
16061
+ const requestedDir = normalizeOptionalString3(options.dir) ?? normalizeOptionalString3(process.env.SNIPARA_WORKSPACE_DIR) ?? process.cwd();
16062
+ const rootDir = resolveRepoRoot3(requestedDir);
16040
16063
  const config = loadConfig({ cwd: rootDir });
16041
16064
  const actor = buildCollaborationActor(options, config);
16042
16065
  return {
@@ -17138,6 +17161,68 @@ function normalizeLocalLeases(value) {
17138
17161
  return lease;
17139
17162
  }).filter((item) => Boolean(item));
17140
17163
  }
17164
+ function normalizeReviewOnlyAcknowledgements(value, now = /* @__PURE__ */ new Date()) {
17165
+ if (!Array.isArray(value)) {
17166
+ return [];
17167
+ }
17168
+ const nowMs = now.getTime();
17169
+ return value.map((item) => {
17170
+ const parsed = item;
17171
+ const profile = normalizeGuardProfile(parsed.profile);
17172
+ const action = normalizeOptionalString3(parsed.action);
17173
+ const findingFingerprint = normalizeOptionalString3(parsed.findingFingerprint);
17174
+ const acknowledgedAt = normalizeOptionalString3(parsed.acknowledgedAt);
17175
+ const expiresAt = normalizeOptionalString3(parsed.expiresAt);
17176
+ if (!profile || !action || !findingFingerprint || !acknowledgedAt || !expiresAt || !Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= nowMs) {
17177
+ return null;
17178
+ }
17179
+ return { profile, action, findingFingerprint, acknowledgedAt, expiresAt };
17180
+ }).filter((item) => Boolean(item));
17181
+ }
17182
+ function expireReviewOnlyAcknowledgements(state, now = /* @__PURE__ */ new Date()) {
17183
+ state.reviewOnlyAcknowledgements = normalizeReviewOnlyAcknowledgements(
17184
+ state.reviewOnlyAcknowledgements,
17185
+ now
17186
+ );
17187
+ }
17188
+ function fingerprintReviewOnlyGuardEvaluation(evaluation) {
17189
+ const findings = evaluation.conflicts.map((conflict) => ({
17190
+ code: conflict.code,
17191
+ decision: conflict.decision,
17192
+ severity: conflict.severity,
17193
+ resource: `${conflict.resource.kind}:${conflict.resource.id}`,
17194
+ actor: conflict.conflictingActor ? `${conflict.conflictingActor.actorType}:${conflict.conflictingActor.actorId}` : null,
17195
+ workSessionId: conflict.workSessionId ?? null,
17196
+ reason: conflict.reason
17197
+ })).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
17198
+ return (0, import_node_crypto6.createHash)("sha256").update(JSON.stringify(findings)).digest("hex");
17199
+ }
17200
+ function persistReviewOnlyAcknowledgement(state, profile, action, findingFingerprint, now = /* @__PURE__ */ new Date()) {
17201
+ invalidateReviewOnlyAcknowledgements(state, profile, action);
17202
+ state.reviewOnlyAcknowledgements.push({
17203
+ profile,
17204
+ action,
17205
+ findingFingerprint,
17206
+ acknowledgedAt: now.toISOString(),
17207
+ expiresAt: new Date(now.getTime() + REVIEW_ONLY_ACK_TTL_MS).toISOString()
17208
+ });
17209
+ }
17210
+ function consumeReviewOnlyAcknowledgement(state, profile, action, findingFingerprint) {
17211
+ expireReviewOnlyAcknowledgements(state);
17212
+ const index = state.reviewOnlyAcknowledgements.findIndex(
17213
+ (ack) => ack.profile === profile && ack.action === action && ack.findingFingerprint === findingFingerprint
17214
+ );
17215
+ if (index < 0) {
17216
+ return false;
17217
+ }
17218
+ state.reviewOnlyAcknowledgements.splice(index, 1);
17219
+ return true;
17220
+ }
17221
+ function invalidateReviewOnlyAcknowledgements(state, profile, action) {
17222
+ state.reviewOnlyAcknowledgements = state.reviewOnlyAcknowledgements.filter(
17223
+ (ack) => ack.profile !== profile || ack.action !== action
17224
+ );
17225
+ }
17141
17226
  function normalizeLastGuard(value) {
17142
17227
  const parsed = value;
17143
17228
  if (!parsed || !parsed.decision || !parsed.checkedAt) {
@@ -37497,7 +37582,7 @@ collaboration.command("guard").description("Check files or resources against act
37497
37582
  "edit"
37498
37583
  ).option("-a, --action <action>", "Guarded action label", "edit").option("--actor <actor>", "Developer or agent display name").option("--actor-id <actorId>", "Stable developer or agent id").option("--actor-type <actorType>", "HUMAN|AGENT|SYSTEM", "AGENT").option("--session-id <sessionId>", "Automation/session id").option("--work-session-id <workSessionId>", "Existing hosted work session id").option("--client <client>", "Client label", "snipara-companion").option("--repository <repository>", "Repository id").option("-b, --branch <branch>", "Current branch name").option("--worktree <worktree>", "Worktree path").option("--max-files <number>", "Maximum files for local code resource expansion", "2000").option("--no-persist", "Evaluate without storing a guard event").option("--enforce", "Exit non-zero for REVIEW_REQUIRED or REQUIRES_ACK, not only BLOCKED").option(
37499
37584
  "--ack-review-only",
37500
- "Under --enforce, acknowledge review-only stale-state and decision-consistency warnings"
37585
+ "Under --enforce, persist one exact review-only acknowledgement for the next hook rerun"
37501
37586
  ).option("-d, --dir <directory>", "Repository directory (default: current)").option("--json", "Print raw JSON").option("--verbose", "Print the full guard report even when the guard passes").action(async (options) => {
37502
37587
  await collaborationGuardCommand({
37503
37588
  files: options.files,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "3.2.29",
3
+ "version": "3.2.30",
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": {