snipara-companion 3.5.7 → 3.5.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  Release notes for `snipara-companion`, newest first.
4
4
 
5
+ ## New In 3.5.9
6
+
7
+ - Routes explicit `workflow phase-commit` and `final-commit` rationale through
8
+ the hosted atomic `why` block instead of issuing a second Why Capture request.
9
+ - Exposes decision, rationale, alternatives, constraints, and observed outcome
10
+ inputs on workflow commits while keeping candidates review-pending.
11
+ - Preserves handoff-only final commits when no structured rationale is supplied.
12
+
13
+ ## New In 3.5.8
14
+
15
+ - Preserves the exact collaboration actor identity on hosted lease heartbeat
16
+ and release requests, preventing owner-authenticated cleanup from falling
17
+ back to the API-key actor and returning a false `403`.
18
+
5
19
  ## New In 3.5.7
6
20
 
7
21
  - Bounds the best-effort hosted judgment lookup in `run` to 8 seconds instead
package/dist/index.d.ts CHANGED
@@ -519,6 +519,13 @@ interface WhyCaptureResult {
519
519
  failed?: Array<Record<string, unknown>>;
520
520
  };
521
521
  }
522
+ interface EndOfTaskCommitWhyInput {
523
+ decision?: string;
524
+ rationale?: string;
525
+ alternatives?: string[];
526
+ constraints?: string[];
527
+ observedOutcome?: string;
528
+ }
522
529
  interface JournalAppendResult {
523
530
  success?: boolean;
524
531
  date?: string;
@@ -1471,7 +1478,7 @@ declare class RLMClient {
1471
1478
  resources?: CollaborationResource[];
1472
1479
  metadata?: Record<string, unknown>;
1473
1480
  }): Promise<CollaborationLeaseResponse>;
1474
- updateCollaborationLease(leaseId: string, args: {
1481
+ updateCollaborationLease(leaseId: string, args: CollaborationActorPayload & {
1475
1482
  action?: "heartbeat" | "release" | "override";
1476
1483
  reason?: string;
1477
1484
  }): Promise<CollaborationLeaseUpdateResponse>;
@@ -1516,6 +1523,8 @@ declare class RLMClient {
1516
1523
  getSessionMemories(maxCriticalTokens?: number, maxDailyTokens?: number, includeYesterday?: boolean): Promise<SessionMemoriesResult>;
1517
1524
  endOfTaskCommit(args: {
1518
1525
  summary: string;
1526
+ task?: string;
1527
+ why?: EndOfTaskCommitWhyInput;
1519
1528
  category?: string;
1520
1529
  outcome?: "completed" | "partial" | "blocked" | "abandoned";
1521
1530
  filesTouched?: string[];
package/dist/index.js CHANGED
@@ -1724,6 +1724,16 @@ var RLMClient = class {
1724
1724
  async endOfTaskCommit(args) {
1725
1725
  return this.mcpCall("snipara_end_of_task_commit", {
1726
1726
  summary: args.summary,
1727
+ ...args.task ? { task: args.task } : {},
1728
+ ...args.why ? {
1729
+ why: {
1730
+ ...args.why.decision ? { decision: args.why.decision } : {},
1731
+ ...args.why.rationale ? { rationale: args.why.rationale } : {},
1732
+ ...args.why.alternatives ? { alternatives: args.why.alternatives } : {},
1733
+ ...args.why.constraints ? { constraints: args.why.constraints } : {},
1734
+ ...args.why.observedOutcome ? { observed_outcome: args.why.observedOutcome } : {}
1735
+ }
1736
+ } : {},
1727
1737
  category: args.category,
1728
1738
  outcome: args.outcome || "completed",
1729
1739
  files_touched: args.filesTouched || [],
@@ -12285,6 +12295,79 @@ function uniqueStrings12(values) {
12285
12295
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
12286
12296
  }
12287
12297
 
12298
+ // ../project-intelligence-contracts/src/why-capture-confidence-v1.json
12299
+ var why_capture_confidence_v1_default = {
12300
+ version: "why-capture-confidence-v1",
12301
+ base: 0.72,
12302
+ decisionRationaleBonus: 0.1,
12303
+ nonManualSourceBonus: 0.04,
12304
+ maximum: 0.9,
12305
+ decimalPlaces: 2,
12306
+ validatedExecutionOutcomes: ["completed"],
12307
+ vectors: [
12308
+ {
12309
+ candidateType: "CONTEXT",
12310
+ hasDecision: false,
12311
+ hasRationale: false,
12312
+ sourceKind: "manual",
12313
+ executionOutcome: "completed",
12314
+ expected: 0.72
12315
+ },
12316
+ {
12317
+ candidateType: "DECISION",
12318
+ hasDecision: true,
12319
+ hasRationale: true,
12320
+ sourceKind: "manual",
12321
+ executionOutcome: "completed",
12322
+ expected: 0.82
12323
+ },
12324
+ {
12325
+ candidateType: "DECISION",
12326
+ hasDecision: true,
12327
+ hasRationale: true,
12328
+ sourceKind: "phase_commit",
12329
+ executionOutcome: "completed",
12330
+ expected: 0.86
12331
+ },
12332
+ {
12333
+ candidateType: "DECISION",
12334
+ hasDecision: true,
12335
+ hasRationale: true,
12336
+ sourceKind: "phase_commit",
12337
+ executionOutcome: "partial",
12338
+ expected: 0.76
12339
+ },
12340
+ {
12341
+ candidateType: "DECISION",
12342
+ hasDecision: true,
12343
+ hasRationale: true,
12344
+ sourceKind: "phase_commit",
12345
+ executionOutcome: "blocked",
12346
+ expected: 0.76
12347
+ },
12348
+ {
12349
+ candidateType: "DECISION",
12350
+ hasDecision: true,
12351
+ hasRationale: true,
12352
+ sourceKind: "phase_commit",
12353
+ executionOutcome: "abandoned",
12354
+ expected: 0.76
12355
+ },
12356
+ {
12357
+ candidateType: "DECISION",
12358
+ hasDecision: true,
12359
+ hasRationale: false,
12360
+ sourceKind: "final_commit",
12361
+ executionOutcome: "completed",
12362
+ expected: 0.76
12363
+ }
12364
+ ]
12365
+ };
12366
+
12367
+ // ../project-intelligence-contracts/src/why-capture-confidence.ts
12368
+ var WHY_CAPTURE_CONFIDENCE_VERSION = why_capture_confidence_v1_default.version;
12369
+ var WHY_CAPTURE_CONFIDENCE_TEST_VECTORS = why_capture_confidence_v1_default.vectors;
12370
+
12288
12371
  // src/commands/decision-requests.ts
12289
12372
  var fs12 = __toESM(require("fs"));
12290
12373
  var path11 = __toESM(require("path"));
@@ -16659,7 +16742,12 @@ async function heartbeatHostedLeases(context, leaseIds) {
16659
16742
  }
16660
16743
  try {
16661
16744
  const data = await Promise.all(
16662
- leaseIds.map((leaseId) => client.updateCollaborationLease(leaseId, { action: "heartbeat" }))
16745
+ leaseIds.map(
16746
+ (leaseId) => client.updateCollaborationLease(leaseId, {
16747
+ ...context.actor,
16748
+ action: "heartbeat"
16749
+ })
16750
+ )
16663
16751
  );
16664
16752
  return { status: "ok", data };
16665
16753
  } catch (error) {
@@ -16675,6 +16763,7 @@ async function releaseHostedLeases(context, leaseIds, input) {
16675
16763
  const data = await Promise.all(
16676
16764
  leaseIds.map(
16677
16765
  (leaseId) => client.updateCollaborationLease(leaseId, {
16766
+ ...context.actor,
16678
16767
  action: "release",
16679
16768
  reason: input.reason
16680
16769
  })
@@ -17822,16 +17911,16 @@ function previewCandidateReceipt(value) {
17822
17911
  if (!isRecord9(value)) {
17823
17912
  return void 0;
17824
17913
  }
17825
- const whyFields = isRecord9(value.whyFields) ? value.whyFields : void 0;
17914
+ const whyFields = isRecord9(value.whyFields) ? value.whyFields : isRecord9(value.why_fields) ? value.why_fields : void 0;
17826
17915
  const decision = boundedText(whyFields?.decision);
17827
- const rationale = boundedText(whyFields?.why);
17828
- const text = boundedText(value.content) ?? decision;
17916
+ const rationale = boundedText(whyFields?.why ?? whyFields?.rationale);
17917
+ const text = boundedText(value.content ?? value.text) ?? decision;
17829
17918
  if (!text) {
17830
17919
  return void 0;
17831
17920
  }
17832
17921
  return {
17833
17922
  text,
17834
- ...boundedText(value.type, 80) ? { type: boundedText(value.type, 80) } : {},
17923
+ ...boundedText(value.type ?? value.memory_type, 80) ? { type: boundedText(value.type ?? value.memory_type, 80) } : {},
17835
17924
  ...boundedText(value.category, 120) ? { category: boundedText(value.category, 120) } : {},
17836
17925
  ...decision ? { decision } : {},
17837
17926
  ...rationale ? { rationale } : {}
@@ -17842,23 +17931,25 @@ function pendingMemoryReceipt(value) {
17842
17931
  return void 0;
17843
17932
  }
17844
17933
  const memoryId = boundedText(value.memory_id ?? value.memoryId ?? value.id, 200);
17845
- const text = boundedText(value.content) ?? (memoryId ? `Memory ${memoryId}` : void 0);
17934
+ const text = boundedText(value.content ?? value.text) ?? (memoryId ? `Memory ${memoryId}` : void 0);
17846
17935
  if (!text) {
17847
17936
  return void 0;
17848
17937
  }
17849
17938
  return {
17850
17939
  ...memoryId ? { memoryId } : {},
17851
17940
  text,
17852
- ...boundedText(value.type, 80) ? { type: boundedText(value.type, 80) } : {},
17853
- ...boundedText(value.category, 120) ? { category: boundedText(value.category, 120) } : {},
17854
- reviewStatus: boundedText(value.review_status ?? value.reviewStatus, 80) ?? "pending"
17941
+ ...boundedText(value.type ?? value.memory_type, 80) ? { type: boundedText(value.type ?? value.memory_type, 80) } : {},
17942
+ category: boundedText(value.category, 120) ?? "why-capture",
17943
+ reviewStatus: boundedText(value.review_status ?? value.reviewStatus, 80) ?? "PENDING"
17855
17944
  };
17856
17945
  }
17857
17946
  function issueReceipt(value, fallbackReason) {
17858
17947
  if (!isRecord9(value)) {
17859
17948
  return void 0;
17860
17949
  }
17861
- const text = boundedText(value.content ?? value.decision ?? value.title ?? value.error ?? value.reason) ?? "Why Capture item";
17950
+ const text = boundedText(
17951
+ value.content ?? value.decision ?? value.title ?? value.error ?? value.reason ?? value.decision_id ?? value.id
17952
+ ) ?? "Why Capture item";
17862
17953
  const reason = boundedText(value.reason ?? value.error, 160) ?? fallbackReason;
17863
17954
  return { text, ...reason ? { reason } : {} };
17864
17955
  }
@@ -17896,6 +17987,47 @@ function readLatestWorkflowCommands(cwd) {
17896
17987
  }
17897
17988
  return [];
17898
17989
  }
17990
+ function recordItems(value) {
17991
+ return Array.isArray(value) ? value.filter(isRecord9) : [];
17992
+ }
17993
+ function taskCommitWhyCaptureReceipt(result, sourceKind, cwd = process.cwd()) {
17994
+ const commitSha = readCommitSha(cwd);
17995
+ const candidates = recordItems(result.candidates).filter(
17996
+ (candidate) => isRecord9(candidate.why_fields) || isRecord9(candidate.whyFields)
17997
+ );
17998
+ const storedCandidates = recordItems(result.stored_candidates).filter(
17999
+ (candidate) => isRecord9(candidate.why_fields) || isRecord9(candidate.whyFields)
18000
+ );
18001
+ const decisionCapture = isRecord9(result.decision_capture) ? result.decision_capture : {};
18002
+ const duplicates = recordItems(decisionCapture.duplicates).map((item) => issueReceipt(item, "duplicate")).filter((item) => Boolean(item));
18003
+ const failed = recordItems(decisionCapture.failed).map((item) => issueReceipt(item, "capture_failed")).filter((item) => Boolean(item));
18004
+ const caveats = Array.isArray(result.caveats) ? result.caveats.filter((item) => typeof item === "string") : [];
18005
+ const hostedPhaseCommit = isRecord9(result.hosted_phase_commit) ? result.hosted_phase_commit : void 0;
18006
+ const hostedFinalCommit = isRecord9(result.hosted_final_commit) ? result.hosted_final_commit : void 0;
18007
+ const hostedError = boundedText(hostedPhaseCommit?.error ?? hostedFinalCommit?.error);
18008
+ const previewCandidates = candidates.map(previewCandidateReceipt).filter((item) => Boolean(item));
18009
+ const pendingMemories = storedCandidates.map(pendingMemoryReceipt).filter((item) => Boolean(item));
18010
+ const baseReceipt = {
18011
+ sourceKind,
18012
+ previewCandidateCount: previewCandidates.length,
18013
+ capturedCount: pendingMemories.length,
18014
+ previewCandidates,
18015
+ pendingMemories,
18016
+ duplicates,
18017
+ failed,
18018
+ ...commitSha ? { commitSha } : {}
18019
+ };
18020
+ if (hostedError) {
18021
+ return { ...baseReceipt, status: "error", error: hostedError };
18022
+ }
18023
+ if (caveats.includes("why_requires_human_owner")) {
18024
+ return { ...baseReceipt, status: "skipped", error: "why_requires_human_owner" };
18025
+ }
18026
+ if (candidates.length === 0) {
18027
+ return { ...baseReceipt, status: "no_candidates" };
18028
+ }
18029
+ return { ...baseReceipt, status: "captured" };
18030
+ }
17899
18031
  async function captureCompanionWhy(input) {
17900
18032
  const cwd = input.cwd ?? process.cwd();
17901
18033
  const commitSha = readCommitSha(cwd);
@@ -20660,7 +20792,7 @@ function recordList(value) {
20660
20792
  return Array.isArray(value) ? value.filter(isRecord11) : [];
20661
20793
  }
20662
20794
  function buildWorkflowPhaseCommitReceipt(input) {
20663
- const stored = recordList(input.result.stored_candidates).map((item) => normalizedMemoryItem(item, "phase_commit", input.phaseId)).filter((item) => Boolean(item));
20795
+ const stored = recordList(input.result.stored_candidates).filter((item) => !isRecord11(item.why_fields) && !isRecord11(item.whyFields)).map((item) => normalizedMemoryItem(item, "phase_commit", input.phaseId)).filter((item) => Boolean(item));
20664
20796
  const skipped = recordList(input.result.skipped_candidates).map((item) => normalizedMemoryItem(item, "phase_commit", input.phaseId)).filter((item) => Boolean(item));
20665
20797
  const hostedPhaseCommit = isRecord11(input.result.hosted_phase_commit) ? input.result.hosted_phase_commit : void 0;
20666
20798
  return {
@@ -32147,6 +32279,8 @@ async function commitTaskMemory(options) {
32147
32279
  const client = createClient(TASK_COMMIT_TIMEOUT_MS);
32148
32280
  return client.endOfTaskCommit({
32149
32281
  summary: options.summary,
32282
+ task: options.task,
32283
+ why: options.why,
32150
32284
  category: options.category,
32151
32285
  outcome: options.outcome,
32152
32286
  filesTouched: options.files
@@ -32219,9 +32353,11 @@ async function commitFinalTaskMemory(options) {
32219
32353
  });
32220
32354
  const callHosted = async (summary, timeoutMs) => {
32221
32355
  const client = createClient(timeoutMs);
32222
- const handoffOnly = isFinalCommitCategory(category);
32356
+ const handoffOnly = isFinalCommitCategory(category) && !options.why;
32223
32357
  return client.endOfTaskCommit({
32224
32358
  summary,
32359
+ task: options.task,
32360
+ why: options.why,
32225
32361
  category,
32226
32362
  outcome: options.outcome,
32227
32363
  filesTouched: options.files,
@@ -32275,6 +32411,28 @@ async function commitFinalTaskMemory(options) {
32275
32411
  message: "Hosted final-commit failed; local fallback handoff created"
32276
32412
  };
32277
32413
  }
32414
+ function structuredTaskCommitWhy(options) {
32415
+ const rationale = compactWhitespace2(options.why ?? "");
32416
+ const explicitDecision = compactWhitespace2(options.decision ?? "");
32417
+ const alternatives = [
32418
+ ...new Set((options.alternatives ?? []).map(compactWhitespace2).filter(Boolean))
32419
+ ];
32420
+ const constraints = [
32421
+ ...new Set((options.constraints ?? []).map(compactWhitespace2).filter(Boolean))
32422
+ ];
32423
+ const observedOutcome = compactWhitespace2(options.observedOutcome ?? "");
32424
+ const decision = explicitDecision || (rationale ? compactWhitespace2(options.summary) : "");
32425
+ if (!decision && !rationale && alternatives.length === 0 && constraints.length === 0 && !observedOutcome) {
32426
+ return void 0;
32427
+ }
32428
+ return {
32429
+ ...decision ? { decision } : {},
32430
+ ...rationale ? { rationale } : {},
32431
+ ...alternatives.length > 0 ? { alternatives } : {},
32432
+ ...constraints.length > 0 ? { constraints } : {},
32433
+ ...observedOutcome ? { observedOutcome } : {}
32434
+ };
32435
+ }
32278
32436
  function printJournalWarning(result) {
32279
32437
  if (result?.status === "error" && result.error) {
32280
32438
  console.log(`Journal checkpoint: ${result.error}`);
@@ -32320,8 +32478,11 @@ async function workflowPhaseCommitCommand(options) {
32320
32478
  phase,
32321
32479
  summary: options.summary
32322
32480
  });
32481
+ const structuredWhy = structuredTaskCommitWhy(options);
32323
32482
  const result = await commitPhaseTaskMemory({
32324
32483
  summary: durableSummary,
32484
+ task: `${state.goal} / ${phase.title}`,
32485
+ why: structuredWhy,
32325
32486
  category,
32326
32487
  outcome,
32327
32488
  files
@@ -32396,14 +32557,7 @@ async function workflowPhaseCommitCommand(options) {
32396
32557
  journalAttempted: true,
32397
32558
  teamSyncCompletionAttempted: shouldCompleteTeamSyncWork
32398
32559
  });
32399
- const whyCapture = await captureCompanionWhy({
32400
- sourceKind: "phase_commit",
32401
- sourceSessionId: state.workflowId,
32402
- task: `${state.goal} / ${phase.title}`,
32403
- summary: options.summary,
32404
- files,
32405
- commands: readLatestWorkflowCommands(process.cwd())
32406
- });
32560
+ const whyCapture = taskCommitWhyCaptureReceipt(result, "phase_commit");
32407
32561
  phaseCommitReceipt.whyCapture = whyCapture;
32408
32562
  writeWorkflowState(state);
32409
32563
  const coordinationRelease = await releaseWorkflowCoordination(
@@ -32501,8 +32655,11 @@ async function finalCommitCommand(options) {
32501
32655
  });
32502
32656
  const outcome = options.outcome ?? "completed";
32503
32657
  const category = normalizeFinalCommitCategory(options.category);
32658
+ const structuredWhy = structuredTaskCommitWhy(options);
32504
32659
  const result = await commitFinalTaskMemory({
32505
32660
  workflowId: state?.workflowId,
32661
+ task: state?.goal,
32662
+ why: structuredWhy,
32506
32663
  summary: options.summary,
32507
32664
  category,
32508
32665
  outcome,
@@ -32554,15 +32711,7 @@ async function finalCommitCommand(options) {
32554
32711
  journalAttempted: true,
32555
32712
  teamSyncCompletionAttempted: outcome === "completed"
32556
32713
  });
32557
- const whyCapture = await captureCompanionWhy({
32558
- sourceKind: "final_commit",
32559
- sourceSessionId: state?.workflowId ?? loadConfig().sessionId,
32560
- task: state?.goal,
32561
- summary: options.summary,
32562
- why: options.why,
32563
- files: options.files,
32564
- commands: readLatestWorkflowCommands(process.cwd())
32565
- });
32714
+ const whyCapture = taskCommitWhyCaptureReceipt(result, "final_commit");
32566
32715
  const coordinationRelease = state ? await releaseWorkflowCoordination(state, `Workflow ${state.workflowId} final-commit.`) : void 0;
32567
32716
  const report = buildFinalCommitReport({
32568
32717
  state,
@@ -39955,7 +40104,7 @@ workflow.command("runtime-checkpoint").description(
39955
40104
  });
39956
40105
  workflow.command("phase-commit").description(
39957
40106
  "Persist a phase outcome through snipara_end_of_task_commit and advance the workflow"
39958
- ).argument("<phaseId>", "Workflow phase id").requiredOption("-s, --summary <summary>", "Phase outcome summary").option("-c, --category <category>", "Memory category", "workflow-phase").option("-o, --outcome <outcome>", "completed|partial|blocked|abandoned", "completed").option("-f, --files <files...>", "Files touched").option(
40107
+ ).argument("<phaseId>", "Workflow phase id").requiredOption("-s, --summary <summary>", "Phase outcome summary").option("--decision <decision>", "Decision text; defaults to summary when --why is set").option("--why <why>", "Decision rationale; never inferred when absent").option("--alternative <alternative>", "Rejected alternative; repeatable", collectOption, []).option("--constraint <constraint>", "Decision constraint; repeatable", collectOption, []).option("--observed-outcome <outcome>", "Observed result, distinct from execution status").option("-c, --category <category>", "Memory category", "workflow-phase").option("-o, --outcome <outcome>", "completed|partial|blocked|abandoned", "completed").option("-f, --files <files...>", "Files touched").option(
39959
40108
  "--evidence <evidence>",
39960
40109
  "Phase evidence as passed|failed|not-run|unknown:text; repeatable",
39961
40110
  collectOption,
@@ -39964,6 +40113,11 @@ workflow.command("phase-commit").description(
39964
40113
  await workflowPhaseCommitCommand({
39965
40114
  phaseId,
39966
40115
  summary: options.summary,
40116
+ decision: options.decision,
40117
+ why: options.why,
40118
+ alternatives: options.alternative,
40119
+ constraints: options.constraint,
40120
+ observedOutcome: options.observedOutcome,
39967
40121
  category: options.category,
39968
40122
  outcome: options.outcome,
39969
40123
  files: options.files,
@@ -39971,7 +40125,7 @@ workflow.command("phase-commit").description(
39971
40125
  json: options.json
39972
40126
  });
39973
40127
  });
39974
- workflow.command("final-commit").description("Persist the final workflow outcome and close the local workflow state").requiredOption("-s, --summary <summary>", "Final outcome summary").option("--why <why>", "Decision rationale; never inferred when absent").option("-c, --category <category>", "Memory category", "final-commit").option("-o, --outcome <outcome>", "completed|partial|blocked|abandoned", "completed").option("-f, --files <files...>", "Files touched").option(
40128
+ workflow.command("final-commit").description("Persist the final workflow outcome and close the local workflow state").requiredOption("-s, --summary <summary>", "Final outcome summary").option("--decision <decision>", "Decision text; defaults to summary when --why is set").option("--why <why>", "Decision rationale; never inferred when absent").option("--alternative <alternative>", "Rejected alternative; repeatable", collectOption, []).option("--constraint <constraint>", "Decision constraint; repeatable", collectOption, []).option("--observed-outcome <outcome>", "Observed result, distinct from execution status").option("-c, --category <category>", "Memory category", "final-commit").option("-o, --outcome <outcome>", "completed|partial|blocked|abandoned", "completed").option("-f, --files <files...>", "Files touched").option(
39975
40129
  "--evidence <evidence>",
39976
40130
  "Verification evidence as passed|failed|not-run|unknown:text; repeatable",
39977
40131
  collectOption,
@@ -39979,7 +40133,11 @@ workflow.command("final-commit").description("Persist the final workflow outcome
39979
40133
  ).option("--risk <risk>", "Known residual risk; repeatable", collectOption, []).option("--next-step <nextStep>", "Recommended next action").option("--json", "Print raw JSON").action(async (options) => {
39980
40134
  await finalCommitCommand({
39981
40135
  summary: options.summary,
40136
+ decision: options.decision,
39982
40137
  why: options.why,
40138
+ alternatives: options.alternative,
40139
+ constraints: options.constraint,
40140
+ observedOutcome: options.observedOutcome,
39983
40141
  category: options.category,
39984
40142
  outcome: options.outcome,
39985
40143
  files: options.files,
@@ -41004,7 +41162,7 @@ program.command("task-commit").description("Persist durable outcomes after meani
41004
41162
  json: options.json
41005
41163
  });
41006
41164
  });
41007
- program.command("final-commit").description("Persist the final managed workflow outcome through snipara_end_of_task_commit").requiredOption("-s, --summary <summary>", "Final outcome summary").option("--why <why>", "Decision rationale; never inferred when absent").option("-c, --category <category>", "Category", "final-commit").option("-o, --outcome <outcome>", "Outcome", "completed").option("-f, --files <files...>", "Files touched").option(
41165
+ program.command("final-commit").description("Persist the final managed workflow outcome through snipara_end_of_task_commit").requiredOption("-s, --summary <summary>", "Final outcome summary").option("--decision <decision>", "Decision text; defaults to summary when --why is set").option("--why <why>", "Decision rationale; never inferred when absent").option("--alternative <alternative>", "Rejected alternative; repeatable", collectOption, []).option("--constraint <constraint>", "Decision constraint; repeatable", collectOption, []).option("--observed-outcome <outcome>", "Observed result, distinct from execution status").option("-c, --category <category>", "Category", "final-commit").option("-o, --outcome <outcome>", "Outcome", "completed").option("-f, --files <files...>", "Files touched").option(
41008
41166
  "--evidence <evidence>",
41009
41167
  "Verification evidence as passed|failed|not-run|unknown:text; repeatable",
41010
41168
  collectOption,
@@ -41012,7 +41170,11 @@ program.command("final-commit").description("Persist the final managed workflow
41012
41170
  ).option("--risk <risk>", "Known residual risk; repeatable", collectOption, []).option("--next-step <nextStep>", "Recommended next action").option("--json", "Print raw JSON").action(async (options) => {
41013
41171
  await finalCommitCommand({
41014
41172
  summary: options.summary,
41173
+ decision: options.decision,
41015
41174
  why: options.why,
41175
+ alternatives: options.alternative,
41176
+ constraints: options.constraint,
41177
+ observedOutcome: options.observedOutcome,
41016
41178
  category: options.category,
41017
41179
  outcome: options.outcome,
41018
41180
  files: options.files,
@@ -1632,10 +1632,17 @@ Companion separates two concepts:
1632
1632
  `workflow phase-commit` and `final-commit` keep local workflow state moving on transient
1633
1633
  hosted commit timeouts and surface that local fallback explicitly in the result.
1634
1634
 
1635
- `final-commit` itself remains handoff-only. Its seven-section report reads the
1636
- durable phase receipts already stored, marks Why Capture candidates as pending
1637
- review, lists skipped/duplicate/failed items as not persisted, and never treats
1638
- the final summary or handoff as newly approved durable memory.
1635
+ `final-commit` remains handoff-only when no structured rationale is supplied.
1636
+ When `--why` or another structured Why Capture field is present, Companion sends
1637
+ one atomic `why` block with the handoff commit; it does not issue a second capture
1638
+ request. `--decision` overrides the decision text, while `--why` uses the commit
1639
+ summary as the decision fallback. Repeat `--alternative` and `--constraint` as
1640
+ needed, and use `--observed-outcome` only for an observed result rather than the
1641
+ execution status. `workflow phase-commit` exposes the same fields. The resulting
1642
+ candidate remains pending review. The seven-section final report reads durable
1643
+ phase receipts already stored, marks Why Capture candidates as pending review,
1644
+ lists skipped/duplicate/failed items as not persisted, and never treats the final
1645
+ summary or handoff as newly approved durable memory.
1639
1646
 
1640
1647
  Do not call `snipara_end_of_task_commit` mechanically for every Git commit. For risky commits,
1641
1648
  package releases, or retries after failures, run Memory Guard first so the agent sees relevant
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "3.5.7",
3
+ "version": "3.5.9",
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": {