snipara-companion 3.5.8 → 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,14 @@
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
+
5
13
  ## New In 3.5.8
6
14
 
7
15
  - Preserves the exact collaboration actor identity on hosted lease heartbeat
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;
@@ -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 || [],
@@ -17901,16 +17911,16 @@ function previewCandidateReceipt(value) {
17901
17911
  if (!isRecord9(value)) {
17902
17912
  return void 0;
17903
17913
  }
17904
- 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;
17905
17915
  const decision = boundedText(whyFields?.decision);
17906
- const rationale = boundedText(whyFields?.why);
17907
- const text = boundedText(value.content) ?? decision;
17916
+ const rationale = boundedText(whyFields?.why ?? whyFields?.rationale);
17917
+ const text = boundedText(value.content ?? value.text) ?? decision;
17908
17918
  if (!text) {
17909
17919
  return void 0;
17910
17920
  }
17911
17921
  return {
17912
17922
  text,
17913
- ...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) } : {},
17914
17924
  ...boundedText(value.category, 120) ? { category: boundedText(value.category, 120) } : {},
17915
17925
  ...decision ? { decision } : {},
17916
17926
  ...rationale ? { rationale } : {}
@@ -17921,23 +17931,25 @@ function pendingMemoryReceipt(value) {
17921
17931
  return void 0;
17922
17932
  }
17923
17933
  const memoryId = boundedText(value.memory_id ?? value.memoryId ?? value.id, 200);
17924
- const text = boundedText(value.content) ?? (memoryId ? `Memory ${memoryId}` : void 0);
17934
+ const text = boundedText(value.content ?? value.text) ?? (memoryId ? `Memory ${memoryId}` : void 0);
17925
17935
  if (!text) {
17926
17936
  return void 0;
17927
17937
  }
17928
17938
  return {
17929
17939
  ...memoryId ? { memoryId } : {},
17930
17940
  text,
17931
- ...boundedText(value.type, 80) ? { type: boundedText(value.type, 80) } : {},
17932
- ...boundedText(value.category, 120) ? { category: boundedText(value.category, 120) } : {},
17933
- 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"
17934
17944
  };
17935
17945
  }
17936
17946
  function issueReceipt(value, fallbackReason) {
17937
17947
  if (!isRecord9(value)) {
17938
17948
  return void 0;
17939
17949
  }
17940
- 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";
17941
17953
  const reason = boundedText(value.reason ?? value.error, 160) ?? fallbackReason;
17942
17954
  return { text, ...reason ? { reason } : {} };
17943
17955
  }
@@ -17975,6 +17987,47 @@ function readLatestWorkflowCommands(cwd) {
17975
17987
  }
17976
17988
  return [];
17977
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
+ }
17978
18031
  async function captureCompanionWhy(input) {
17979
18032
  const cwd = input.cwd ?? process.cwd();
17980
18033
  const commitSha = readCommitSha(cwd);
@@ -20739,7 +20792,7 @@ function recordList(value) {
20739
20792
  return Array.isArray(value) ? value.filter(isRecord11) : [];
20740
20793
  }
20741
20794
  function buildWorkflowPhaseCommitReceipt(input) {
20742
- 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));
20743
20796
  const skipped = recordList(input.result.skipped_candidates).map((item) => normalizedMemoryItem(item, "phase_commit", input.phaseId)).filter((item) => Boolean(item));
20744
20797
  const hostedPhaseCommit = isRecord11(input.result.hosted_phase_commit) ? input.result.hosted_phase_commit : void 0;
20745
20798
  return {
@@ -32226,6 +32279,8 @@ async function commitTaskMemory(options) {
32226
32279
  const client = createClient(TASK_COMMIT_TIMEOUT_MS);
32227
32280
  return client.endOfTaskCommit({
32228
32281
  summary: options.summary,
32282
+ task: options.task,
32283
+ why: options.why,
32229
32284
  category: options.category,
32230
32285
  outcome: options.outcome,
32231
32286
  filesTouched: options.files
@@ -32298,9 +32353,11 @@ async function commitFinalTaskMemory(options) {
32298
32353
  });
32299
32354
  const callHosted = async (summary, timeoutMs) => {
32300
32355
  const client = createClient(timeoutMs);
32301
- const handoffOnly = isFinalCommitCategory(category);
32356
+ const handoffOnly = isFinalCommitCategory(category) && !options.why;
32302
32357
  return client.endOfTaskCommit({
32303
32358
  summary,
32359
+ task: options.task,
32360
+ why: options.why,
32304
32361
  category,
32305
32362
  outcome: options.outcome,
32306
32363
  filesTouched: options.files,
@@ -32354,6 +32411,28 @@ async function commitFinalTaskMemory(options) {
32354
32411
  message: "Hosted final-commit failed; local fallback handoff created"
32355
32412
  };
32356
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
+ }
32357
32436
  function printJournalWarning(result) {
32358
32437
  if (result?.status === "error" && result.error) {
32359
32438
  console.log(`Journal checkpoint: ${result.error}`);
@@ -32399,8 +32478,11 @@ async function workflowPhaseCommitCommand(options) {
32399
32478
  phase,
32400
32479
  summary: options.summary
32401
32480
  });
32481
+ const structuredWhy = structuredTaskCommitWhy(options);
32402
32482
  const result = await commitPhaseTaskMemory({
32403
32483
  summary: durableSummary,
32484
+ task: `${state.goal} / ${phase.title}`,
32485
+ why: structuredWhy,
32404
32486
  category,
32405
32487
  outcome,
32406
32488
  files
@@ -32475,14 +32557,7 @@ async function workflowPhaseCommitCommand(options) {
32475
32557
  journalAttempted: true,
32476
32558
  teamSyncCompletionAttempted: shouldCompleteTeamSyncWork
32477
32559
  });
32478
- const whyCapture = await captureCompanionWhy({
32479
- sourceKind: "phase_commit",
32480
- sourceSessionId: state.workflowId,
32481
- task: `${state.goal} / ${phase.title}`,
32482
- summary: options.summary,
32483
- files,
32484
- commands: readLatestWorkflowCommands(process.cwd())
32485
- });
32560
+ const whyCapture = taskCommitWhyCaptureReceipt(result, "phase_commit");
32486
32561
  phaseCommitReceipt.whyCapture = whyCapture;
32487
32562
  writeWorkflowState(state);
32488
32563
  const coordinationRelease = await releaseWorkflowCoordination(
@@ -32580,8 +32655,11 @@ async function finalCommitCommand(options) {
32580
32655
  });
32581
32656
  const outcome = options.outcome ?? "completed";
32582
32657
  const category = normalizeFinalCommitCategory(options.category);
32658
+ const structuredWhy = structuredTaskCommitWhy(options);
32583
32659
  const result = await commitFinalTaskMemory({
32584
32660
  workflowId: state?.workflowId,
32661
+ task: state?.goal,
32662
+ why: structuredWhy,
32585
32663
  summary: options.summary,
32586
32664
  category,
32587
32665
  outcome,
@@ -32633,15 +32711,7 @@ async function finalCommitCommand(options) {
32633
32711
  journalAttempted: true,
32634
32712
  teamSyncCompletionAttempted: outcome === "completed"
32635
32713
  });
32636
- const whyCapture = await captureCompanionWhy({
32637
- sourceKind: "final_commit",
32638
- sourceSessionId: state?.workflowId ?? loadConfig().sessionId,
32639
- task: state?.goal,
32640
- summary: options.summary,
32641
- why: options.why,
32642
- files: options.files,
32643
- commands: readLatestWorkflowCommands(process.cwd())
32644
- });
32714
+ const whyCapture = taskCommitWhyCaptureReceipt(result, "final_commit");
32645
32715
  const coordinationRelease = state ? await releaseWorkflowCoordination(state, `Workflow ${state.workflowId} final-commit.`) : void 0;
32646
32716
  const report = buildFinalCommitReport({
32647
32717
  state,
@@ -40034,7 +40104,7 @@ workflow.command("runtime-checkpoint").description(
40034
40104
  });
40035
40105
  workflow.command("phase-commit").description(
40036
40106
  "Persist a phase outcome through snipara_end_of_task_commit and advance the workflow"
40037
- ).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(
40038
40108
  "--evidence <evidence>",
40039
40109
  "Phase evidence as passed|failed|not-run|unknown:text; repeatable",
40040
40110
  collectOption,
@@ -40043,6 +40113,11 @@ workflow.command("phase-commit").description(
40043
40113
  await workflowPhaseCommitCommand({
40044
40114
  phaseId,
40045
40115
  summary: options.summary,
40116
+ decision: options.decision,
40117
+ why: options.why,
40118
+ alternatives: options.alternative,
40119
+ constraints: options.constraint,
40120
+ observedOutcome: options.observedOutcome,
40046
40121
  category: options.category,
40047
40122
  outcome: options.outcome,
40048
40123
  files: options.files,
@@ -40050,7 +40125,7 @@ workflow.command("phase-commit").description(
40050
40125
  json: options.json
40051
40126
  });
40052
40127
  });
40053
- 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(
40054
40129
  "--evidence <evidence>",
40055
40130
  "Verification evidence as passed|failed|not-run|unknown:text; repeatable",
40056
40131
  collectOption,
@@ -40058,7 +40133,11 @@ workflow.command("final-commit").description("Persist the final workflow outcome
40058
40133
  ).option("--risk <risk>", "Known residual risk; repeatable", collectOption, []).option("--next-step <nextStep>", "Recommended next action").option("--json", "Print raw JSON").action(async (options) => {
40059
40134
  await finalCommitCommand({
40060
40135
  summary: options.summary,
40136
+ decision: options.decision,
40061
40137
  why: options.why,
40138
+ alternatives: options.alternative,
40139
+ constraints: options.constraint,
40140
+ observedOutcome: options.observedOutcome,
40062
40141
  category: options.category,
40063
40142
  outcome: options.outcome,
40064
40143
  files: options.files,
@@ -41083,7 +41162,7 @@ program.command("task-commit").description("Persist durable outcomes after meani
41083
41162
  json: options.json
41084
41163
  });
41085
41164
  });
41086
- 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(
41087
41166
  "--evidence <evidence>",
41088
41167
  "Verification evidence as passed|failed|not-run|unknown:text; repeatable",
41089
41168
  collectOption,
@@ -41091,7 +41170,11 @@ program.command("final-commit").description("Persist the final managed workflow
41091
41170
  ).option("--risk <risk>", "Known residual risk; repeatable", collectOption, []).option("--next-step <nextStep>", "Recommended next action").option("--json", "Print raw JSON").action(async (options) => {
41092
41171
  await finalCommitCommand({
41093
41172
  summary: options.summary,
41173
+ decision: options.decision,
41094
41174
  why: options.why,
41175
+ alternatives: options.alternative,
41176
+ constraints: options.constraint,
41177
+ observedOutcome: options.observedOutcome,
41095
41178
  category: options.category,
41096
41179
  outcome: options.outcome,
41097
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.8",
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": {