snipara-companion 1.3.1 → 1.3.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/README.md CHANGED
@@ -112,6 +112,14 @@ The mental model is intentionally close to Git:
112
112
  | `git format-patch` | `snipara-companion handoff` |
113
113
  | `git checkout` | `snipara-companion workflow resume` |
114
114
 
115
+ `snipara-companion final-commit` closes the local workflow and asks the hosted
116
+ API only for the final Team Sync handoff. The CLI sends a compact summary with a
117
+ longer timeout, retries once with a shorter summary on transient hosted failures,
118
+ and then records a local fallback handoff in `.snipara/team-sync/session.json`
119
+ if the hosted call still times out. A hosted final-commit timeout does not modify
120
+ Git state. Custom final-commit categories are namespaced under `final-commit`
121
+ before the hosted call so they stay on the handoff-only path.
122
+
115
123
  ## Verification Plans
116
124
 
117
125
  Use `verify` when an agent asks what to prove before handoff or release:
package/dist/index.d.ts CHANGED
@@ -949,6 +949,8 @@ declare class RLMClient {
949
949
  category?: string;
950
950
  outcome?: "completed" | "partial" | "blocked" | "abandoned";
951
951
  filesTouched?: string[];
952
+ persistTypes?: string[];
953
+ handoffOnly?: boolean;
952
954
  }): Promise<Record<string, unknown>>;
953
955
  journalAppend(text: string, tags?: string[]): Promise<JournalAppendResult>;
954
956
  }
package/dist/index.js CHANGED
@@ -1304,7 +1304,8 @@ var RLMClient = class {
1304
1304
  category: args.category,
1305
1305
  outcome: args.outcome || "completed",
1306
1306
  files_touched: args.filesTouched || [],
1307
- persist_types: ["decision", "learning", "workflow"]
1307
+ persist_types: args.persistTypes ?? ["decision", "learning", "workflow"],
1308
+ ...args.handoffOnly !== void 0 ? { handoff_only: args.handoffOnly } : {}
1308
1309
  });
1309
1310
  }
1310
1311
  async journalAppend(text, tags) {
@@ -8234,6 +8235,11 @@ var import_chalk5 = __toESM(require("chalk"));
8234
8235
  var DEFAULT_SESSION_CONTEXT_TOKENS = 1e3;
8235
8236
  var DEFAULT_FULL_WORKFLOW_CRITICAL_TOKENS = 2e3;
8236
8237
  var DEFAULT_SHARED_CONTEXT_TOKENS = 2e3;
8238
+ var TASK_COMMIT_TIMEOUT_MS = 3e4;
8239
+ var FINAL_COMMIT_TIMEOUT_MS = 9e4;
8240
+ var FINAL_COMMIT_RETRY_TIMEOUT_MS = 45e3;
8241
+ var FINAL_COMMIT_SUMMARY_MAX_CHARS = 1200;
8242
+ var FINAL_COMMIT_RETRY_SUMMARY_MAX_CHARS = 600;
8237
8243
  var SHARED_CONTEXT_INTENT_PATTERN = /\b(standard|standards|convention|conventions|guideline|guidelines|best practice|best practices|policy|policies|compliance|compliant|security rules|team rules|style guide|playbook|checklist)\b/i;
8238
8244
  var DOCUMENT_SYNC_FORMATS = {
8239
8245
  ".adoc": { kind: "DOC", format: "adoc" },
@@ -8469,6 +8475,52 @@ function toPreview(value, maxLength = 160) {
8469
8475
  }
8470
8476
  return "n/a";
8471
8477
  }
8478
+ function positiveIntegerEnv(name, fallback) {
8479
+ const value = process.env[name];
8480
+ if (!value) {
8481
+ return fallback;
8482
+ }
8483
+ const parsed = Number.parseInt(value, 10);
8484
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
8485
+ }
8486
+ function compactWhitespace(value) {
8487
+ return value.replace(/\s+/g, " ").trim();
8488
+ }
8489
+ function truncateText(value, maxLength) {
8490
+ const normalized = compactWhitespace(value);
8491
+ if (normalized.length <= maxLength) {
8492
+ return normalized;
8493
+ }
8494
+ const suffix = " ... [truncated locally for hosted final-commit]";
8495
+ return `${normalized.slice(0, Math.max(0, maxLength - suffix.length)).trimEnd()}${suffix}`;
8496
+ }
8497
+ function buildHostedFinalCommitSummary(args) {
8498
+ const prefix = args.workflowId ? `Workflow ${args.workflowId}
8499
+ Final commit
8500
+ ` : "";
8501
+ const budget = Math.max(80, args.maxLength - prefix.length);
8502
+ return `${prefix}${truncateText(args.summary, budget)}`;
8503
+ }
8504
+ function hostedCommitErrorMessage(error) {
8505
+ return error instanceof Error ? error.message : String(error);
8506
+ }
8507
+ function shouldRetryHostedFinalCommit(error) {
8508
+ const message = hostedCommitErrorMessage(error).toLowerCase();
8509
+ if (error instanceof Error && error.name === "AbortError") {
8510
+ return true;
8511
+ }
8512
+ return /abort|timeout|timed out|network|fetch|econn|etimedout|http 5\d\d/.test(message);
8513
+ }
8514
+ function isFinalCommitCategory(category) {
8515
+ return Boolean(category?.toLowerCase().includes("final-commit"));
8516
+ }
8517
+ function normalizeFinalCommitCategory(category) {
8518
+ const normalized = compactWhitespace(category ?? "");
8519
+ if (!normalized) {
8520
+ return "final-commit";
8521
+ }
8522
+ return isFinalCommitCategory(normalized) ? normalized : `final-commit:${normalized}`;
8523
+ }
8472
8524
  function getPlanStepDisplayTitle(step, index = 0) {
8473
8525
  if (typeof step === "string") {
8474
8526
  return toPreview(step);
@@ -12185,7 +12237,7 @@ function printWorkflowTeamSyncResume(result) {
12185
12237
  }
12186
12238
  async function commitTaskMemory(options) {
12187
12239
  ensureConfigured();
12188
- const client = createClient(3e4);
12240
+ const client = createClient(TASK_COMMIT_TIMEOUT_MS);
12189
12241
  return client.endOfTaskCommit({
12190
12242
  summary: options.summary,
12191
12243
  category: options.category,
@@ -12193,6 +12245,101 @@ async function commitTaskMemory(options) {
12193
12245
  filesTouched: options.files
12194
12246
  });
12195
12247
  }
12248
+ function recordLocalFinalCommitHandoff(options) {
12249
+ const rootDir = process.cwd();
12250
+ autoArchiveTeamSyncState(rootDir);
12251
+ const state = loadTeamSyncState(rootDir);
12252
+ const record = buildTeamSyncHandoffRecord({
12253
+ summary: truncateText(options.summary, FINAL_COMMIT_SUMMARY_MAX_CHARS),
12254
+ files: options.files,
12255
+ attention: options.outcome === "completed" ? "watch" : "proof",
12256
+ next: options.outcome === "completed" ? "Review this local fallback handoff before starting follow-up work." : "Resolve the blocker captured in this local fallback handoff."
12257
+ });
12258
+ state.handoffs.push(record);
12259
+ state.updatedAt = record.createdAt;
12260
+ saveTeamSyncState(state, rootDir);
12261
+ return {
12262
+ status: "local_fallback",
12263
+ record_id: record.id,
12264
+ state_path: getTeamSyncStatePath(rootDir),
12265
+ category: "team_sync_handoff",
12266
+ source_session_id: "local-companion-fallback",
12267
+ files: record.files,
12268
+ error: options.error
12269
+ };
12270
+ }
12271
+ async function commitFinalTaskMemory(options) {
12272
+ ensureConfigured();
12273
+ const category = normalizeFinalCommitCategory(options.category);
12274
+ const attempts = [];
12275
+ const primarySummary = buildHostedFinalCommitSummary({
12276
+ workflowId: options.workflowId,
12277
+ summary: options.summary,
12278
+ maxLength: FINAL_COMMIT_SUMMARY_MAX_CHARS
12279
+ });
12280
+ const retrySummary = buildHostedFinalCommitSummary({
12281
+ workflowId: options.workflowId,
12282
+ summary: options.summary,
12283
+ maxLength: FINAL_COMMIT_RETRY_SUMMARY_MAX_CHARS
12284
+ });
12285
+ const callHosted = async (summary, timeoutMs) => {
12286
+ const client = createClient(timeoutMs);
12287
+ const handoffOnly = isFinalCommitCategory(category);
12288
+ return client.endOfTaskCommit({
12289
+ summary,
12290
+ category,
12291
+ outcome: options.outcome,
12292
+ filesTouched: options.files,
12293
+ persistTypes: handoffOnly ? [] : ["decision", "learning", "workflow"],
12294
+ handoffOnly
12295
+ });
12296
+ };
12297
+ try {
12298
+ return await callHosted(
12299
+ primarySummary,
12300
+ positiveIntegerEnv("SNIPARA_FINAL_COMMIT_TIMEOUT_MS", FINAL_COMMIT_TIMEOUT_MS)
12301
+ );
12302
+ } catch (error) {
12303
+ attempts.push({
12304
+ summary_chars: primarySummary.length,
12305
+ error: hostedCommitErrorMessage(error)
12306
+ });
12307
+ if (shouldRetryHostedFinalCommit(error)) {
12308
+ try {
12309
+ return await callHosted(
12310
+ retrySummary,
12311
+ positiveIntegerEnv("SNIPARA_FINAL_COMMIT_RETRY_TIMEOUT_MS", FINAL_COMMIT_RETRY_TIMEOUT_MS)
12312
+ );
12313
+ } catch (retryError) {
12314
+ attempts.push({
12315
+ summary_chars: retrySummary.length,
12316
+ error: hostedCommitErrorMessage(retryError)
12317
+ });
12318
+ }
12319
+ }
12320
+ }
12321
+ const lastError = attempts[attempts.length - 1]?.error ?? "hosted final-commit failed";
12322
+ const localHandoff = recordLocalFinalCommitHandoff({
12323
+ summary: options.summary,
12324
+ outcome: options.outcome,
12325
+ files: options.files,
12326
+ error: lastError
12327
+ });
12328
+ return {
12329
+ stored_count: 0,
12330
+ skipped_count: 0,
12331
+ candidates: [],
12332
+ stored_candidates: [],
12333
+ skipped_candidates: [],
12334
+ team_sync_handoff: localHandoff,
12335
+ hosted_final_commit: {
12336
+ status: "error",
12337
+ attempts,
12338
+ message: "Hosted snipara_end_of_task_commit failed; local workflow state and Team Sync fallback handoff were preserved."
12339
+ },
12340
+ message: "Hosted final-commit failed; local fallback handoff created"
12341
+ };
12342
+ }
12196
12343
  function printJournalWarning(result) {
12197
12344
  if (result?.status === "error" && result.error) {
12198
12345
  console.log(`Journal checkpoint: ${result.error}`);
@@ -12266,10 +12413,11 @@ async function finalCommitCommand(options) {
12266
12413
  });
12267
12414
  const state = readWorkflowState2();
12268
12415
  const outcome = options.outcome ?? "completed";
12269
- const summary = state ? [`Workflow ${state.workflowId}`, "Final commit", options.summary].join("\n") : options.summary;
12270
- const result = await commitTaskMemory({
12271
- summary,
12272
- category: options.category ?? "final-commit",
12416
+ const category = normalizeFinalCommitCategory(options.category);
12417
+ const result = await commitFinalTaskMemory({
12418
+ workflowId: state?.workflowId,
12419
+ summary: options.summary,
12420
+ category,
12273
12421
  outcome,
12274
12422
  files: options.files
12275
12423
  });
@@ -12279,7 +12427,7 @@ async function finalCommitCommand(options) {
12279
12427
  state.currentPhaseId = outcome === "completed" ? void 0 : state.currentPhaseId;
12280
12428
  state.updatedAt = now;
12281
12429
  state.lastCommit = {
12282
- category: options.category ?? "final-commit",
12430
+ category,
12283
12431
  outcome,
12284
12432
  summary: options.summary,
12285
12433
  committedAt: now
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Snipara Git-style companion CLI for hosted context, agent continuity, hooks, and automation workflows",
5
5
  "main": "dist/index.js",
6
6
  "bin": {