snipara-companion 3.2.15 → 3.2.17

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
@@ -77,6 +77,7 @@ These commands are useful without hosted Snipara:
77
77
  | `workflow start` / `phase-start` / `phase-commit` / `resume` | Agent continuity that survives compaction |
78
78
  | `workflow timeline` / `workflow session` | Append-only local activity log and Session Snapshot V0 |
79
79
  | `workflow decisions` / `workflow decide` | Local human decision requests and response receipts |
80
+ | `workflow policy-ledger` | Project Policy decisions for agent review and audit |
80
81
  | `run --emit-policy-decisions` | Project Policy review requests in the agent workflow |
81
82
  | `workflow producer-triage` | Ask for human review of unreviewed Producer Loop samples |
82
83
  | `workflow producer-report` | Local Producer Loop adoption and calibration report |
@@ -144,6 +145,7 @@ npx -y snipara-companion lead-plan --from-plan ./project-health-lead-plan.json -
144
145
  npx -y snipara-companion workflow phase-commit audit --summary "mapped auth impact"
145
146
  npx -y snipara-companion workflow producer-triage
146
147
  npx -y snipara-companion workflow decisions
148
+ npx -y snipara-companion workflow policy-ledger
147
149
  npx -y snipara-companion workflow decide decision-abc123 --choose accept_all --reviewer alice
148
150
  npx -y snipara-companion workflow timeline
149
151
  npx -y snipara-companion workflow timeline --export md
@@ -193,6 +195,10 @@ opaque refs. Decision requests never resolve by timeout or default, and only
193
195
  When repeated resolved receipts share the same human choice and rationale,
194
196
  `workflow decide` may emit a new review-only policy suggestion decision request;
195
197
  it still uses manual apply instructions and never writes policy automatically.
198
+ `workflow policy-ledger` gives the LLM agent a consolidated view of pending,
199
+ approved, refused, modified, and deferred Project Policy decision artifacts,
200
+ plus the exact pending requests it should ask the human about. It is read-only
201
+ and does not apply policy edits.
196
202
  Other producers such as `outcome-capture preview --emit-decisions`,
197
203
  `memory reviews --emit-decisions`, `workflow decision-producer memory`, and
198
204
  `workflow decision-producer context-risk` emit requests with their existing apply
package/dist/index.d.ts CHANGED
@@ -3149,6 +3149,14 @@ interface AgenticWorkStatus {
3149
3149
  count?: number;
3150
3150
  note: string;
3151
3151
  };
3152
+ operationalLoop: {
3153
+ status: "clear" | "attention" | "blocked";
3154
+ decisionRequestCount: number;
3155
+ receiptGapCount: number;
3156
+ nextActions: string[];
3157
+ receiptActions: string[];
3158
+ caveats: string[];
3159
+ };
3152
3160
  suggestedNextAction: string;
3153
3161
  }
3154
3162
  interface AgenticTimelineEvent {
package/dist/index.js CHANGED
@@ -18917,6 +18917,163 @@ function compactDecisionRequest(request, status) {
18917
18917
  expiresAt: request.expiresAt
18918
18918
  };
18919
18919
  }
18920
+ function isPolicyDecisionRequest(request) {
18921
+ return request.producer.kind === "project_policy_review" || request.producer.kind === "policy_suggestion" || request.evidence.reasonCodes.some((code2) => /policy/i.test(code2)) || /project policy|policy suggestion|policy/i.test(
18922
+ [
18923
+ request.decision,
18924
+ request.question,
18925
+ request.evidence.summary,
18926
+ request.evidence.applyPath ?? ""
18927
+ ].join(" ")
18928
+ );
18929
+ }
18930
+ function classifyResolvedPolicyDecisionChoice(choice) {
18931
+ switch (choice) {
18932
+ case "approve_once":
18933
+ case "accept":
18934
+ case "accept_all":
18935
+ case "create_policy_suggestion":
18936
+ case "keep_advisory":
18937
+ return "approved";
18938
+ case "reject":
18939
+ case "reject_all":
18940
+ case "reject_policy_suggestion":
18941
+ case "require_changes":
18942
+ case "respect_block":
18943
+ return "refused";
18944
+ case "mark_policy_stale":
18945
+ case "request_exception":
18946
+ return "modified";
18947
+ case "ignore_once":
18948
+ case "inspect":
18949
+ case "defer":
18950
+ default:
18951
+ return "deferred";
18952
+ }
18953
+ }
18954
+ function buildPolicyLedgerEntries() {
18955
+ const pending = listPendingDecisionRequests().filter((entry) => isPolicyDecisionRequest(entry.request)).map((entry) => {
18956
+ const request = entry.request;
18957
+ return {
18958
+ requestId: request.requestId,
18959
+ fingerprint: request.fingerprint,
18960
+ status: entry.status,
18961
+ producerKind: request.producer.kind,
18962
+ decision: request.decision,
18963
+ question: request.question,
18964
+ recommendation: request.recommendation,
18965
+ evidenceSummary: request.evidence.summary,
18966
+ reasonCodes: request.evidence.reasonCodes,
18967
+ files: request.evidence.files ?? [],
18968
+ applyPath: request.evidence.applyPath,
18969
+ applyCommand: request.evidence.applyCommand,
18970
+ createdAt: request.createdAt
18971
+ };
18972
+ });
18973
+ const resolved = listResolvedDecisionRecords().filter((record) => isPolicyDecisionRequest(record.request)).map((record) => {
18974
+ const request = record.request;
18975
+ return {
18976
+ requestId: request.requestId,
18977
+ fingerprint: request.fingerprint,
18978
+ status: classifyResolvedPolicyDecisionChoice(record.response.choice),
18979
+ producerKind: request.producer.kind,
18980
+ decision: request.decision,
18981
+ question: request.question,
18982
+ recommendation: request.recommendation,
18983
+ humanChoice: record.response.choice,
18984
+ reviewer: record.response.reviewer,
18985
+ note: record.response.note,
18986
+ evidenceSummary: request.evidence.summary,
18987
+ reasonCodes: request.evidence.reasonCodes,
18988
+ files: request.evidence.files ?? [],
18989
+ applyPath: request.evidence.applyPath,
18990
+ applyCommand: request.evidence.applyCommand,
18991
+ createdAt: request.createdAt,
18992
+ resolvedAt: record.response.resolvedAt
18993
+ };
18994
+ });
18995
+ return [...pending, ...resolved].sort((left, right) => {
18996
+ const leftTime = left.resolvedAt ?? left.createdAt;
18997
+ const rightTime = right.resolvedAt ?? right.createdAt;
18998
+ return rightTime.localeCompare(leftTime) || left.requestId.localeCompare(right.requestId);
18999
+ });
19000
+ }
19001
+ function summarizePolicyLedger(entries) {
19002
+ const counts = {
19003
+ pending: 0,
19004
+ expired_pending: 0,
19005
+ approved: 0,
19006
+ refused: 0,
19007
+ modified: 0,
19008
+ deferred: 0
19009
+ };
19010
+ for (const entry of entries) {
19011
+ counts[entry.status] += 1;
19012
+ }
19013
+ return {
19014
+ total: entries.length,
19015
+ pending: counts.pending,
19016
+ expiredPending: counts.expired_pending,
19017
+ approved: counts.approved,
19018
+ refused: counts.refused,
19019
+ modified: counts.modified,
19020
+ deferred: counts.deferred
19021
+ };
19022
+ }
19023
+ function buildPolicyLedgerAgentPrompt(entries) {
19024
+ const active = entries.filter(
19025
+ (entry) => entry.status === "pending" || entry.status === "expired_pending"
19026
+ );
19027
+ if (active.length === 0) {
19028
+ return ["No pending Project Policy decision needs a human response right now."];
19029
+ }
19030
+ return active.slice(0, 5).map((entry) => {
19031
+ const options = entry.recommendation ? ` Recommended: ${entry.recommendation}.` : "";
19032
+ return `Ask the user: ${entry.question} Options are recorded in the pending Decision Request.${options} Resolve with snipara-companion workflow decide ${entry.requestId} --choose <human-choice> --reviewer <name>.`;
19033
+ });
19034
+ }
19035
+ async function workflowPolicyLedgerCommand(options) {
19036
+ const entries = buildPolicyLedgerEntries();
19037
+ const summary = summarizePolicyLedger(entries);
19038
+ const payload = {
19039
+ version: "snipara.workflow_policy_ledger.v0",
19040
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
19041
+ summary,
19042
+ entries,
19043
+ agentPrompt: buildPolicyLedgerAgentPrompt(entries),
19044
+ caveats: [
19045
+ "Policy ledger is observational; it never applies or edits Project Policy automatically.",
19046
+ "The LLM agent must ask the user for pending policy decisions and resolve them with workflow decide.",
19047
+ "The dashboard is for auditability and administration, not a replacement for explicit human approval."
19048
+ ]
19049
+ };
19050
+ if (options.json) {
19051
+ printJson2(payload);
19052
+ return;
19053
+ }
19054
+ console.log(import_chalk6.default.bold("Project Policy Ledger"));
19055
+ printKeyValue2("Pending:", String(summary.pending));
19056
+ printKeyValue2("Approved:", String(summary.approved));
19057
+ printKeyValue2("Refused:", String(summary.refused));
19058
+ printKeyValue2("Modified:", String(summary.modified));
19059
+ if (entries.length === 0) {
19060
+ console.log("No Project Policy decision artifacts found.");
19061
+ return;
19062
+ }
19063
+ for (const entry of entries.slice(0, 20)) {
19064
+ const actor = entry.reviewer ? ` by ${entry.reviewer}` : "";
19065
+ console.log(`- ${entry.requestId} [${entry.status}${actor}] ${entry.question}`);
19066
+ if (entry.humanChoice) {
19067
+ console.log(` choice: ${entry.humanChoice}`);
19068
+ }
19069
+ console.log(` evidence: ${entry.evidenceSummary}`);
19070
+ if (entry.status === "pending" || entry.status === "expired_pending") {
19071
+ console.log(
19072
+ ` ask: snipara-companion workflow decide ${entry.requestId} --choose <human-choice> --reviewer <name>`
19073
+ );
19074
+ }
19075
+ }
19076
+ }
18920
19077
  function printDecisionRequests(entries) {
18921
19078
  console.log(import_chalk6.default.bold("Pending Decision Requests"));
18922
19079
  if (entries.length === 0) {
@@ -23292,6 +23449,68 @@ function buildSuggestedAgenticNextAction(state, risks) {
23292
23449
  }
23293
23450
  return "Run snipara-companion workflow status to inspect the managed workflow.";
23294
23451
  }
23452
+ function buildAgenticOperationalLoop(args) {
23453
+ const phase = args.state ? currentWorkflowPhase(args.state) : void 0;
23454
+ const nextActions = [];
23455
+ const receiptActions = [];
23456
+ const caveats = [
23457
+ "Operational Loop is local and advisory; it does not edit Project Policy, approve memory, or bypass verification."
23458
+ ];
23459
+ let receiptGapCount = 0;
23460
+ if (args.pendingDecisionCount > 0) {
23461
+ nextActions.push(
23462
+ `Resolve ${args.pendingDecisionCount} local Decision Request${args.pendingDecisionCount === 1 ? "" : "s"} with snipara-companion workflow decisions and workflow decide.`
23463
+ );
23464
+ }
23465
+ if (args.teamSyncSummary.staleWorkCount > 0) {
23466
+ nextActions.push(
23467
+ "Review stale Team Sync work with snipara-companion team-sync sweep --dry-run before continuing."
23468
+ );
23469
+ }
23470
+ if (args.dirtyFileCount > 0) {
23471
+ nextActions.push("Review dirty git state and run the relevant checks before phase-commit.");
23472
+ }
23473
+ if (args.latestHandoff?.next) {
23474
+ nextActions.push(
23475
+ `Continue latest handoff next step: ${toPreview(args.latestHandoff.next, 160)}`
23476
+ );
23477
+ }
23478
+ if (phase?.status === "blocked") {
23479
+ nextActions.push(`Unblock current phase '${phase.id}' before new implementation work.`);
23480
+ } else if (phase) {
23481
+ nextActions.push(
23482
+ `Continue phase '${phase.id}' and close it with snipara-companion workflow phase-commit ${phase.id}.`
23483
+ );
23484
+ } else if (!args.state) {
23485
+ nextActions.push("Start a managed workflow before multi-step implementation work.");
23486
+ }
23487
+ if (args.state && args.state.status !== "completed") {
23488
+ receiptGapCount += 1;
23489
+ receiptActions.push(
23490
+ "Before closing the phase, capture why/outcome evidence with snipara-companion outcome-capture preview --emit-outcome-receipt."
23491
+ );
23492
+ }
23493
+ if (args.latestHandoff && args.latestHandoff.attention !== "note") {
23494
+ receiptGapCount += 1;
23495
+ receiptActions.push(
23496
+ "Treat the latest Team Sync handoff as needing proof/review evidence before final-commit."
23497
+ );
23498
+ }
23499
+ if (args.pendingDecisionCount > 0) {
23500
+ receiptActions.push(
23501
+ "Decision Request resolution records the human choice; follow-up policy edits remain explicit."
23502
+ );
23503
+ }
23504
+ const status = phase?.status === "blocked" ? "blocked" : args.risks.length > 0 || args.pendingDecisionCount > 0 || receiptGapCount > 0 ? "attention" : "clear";
23505
+ return {
23506
+ status,
23507
+ decisionRequestCount: args.pendingDecisionCount,
23508
+ receiptGapCount,
23509
+ nextActions: uniqueStrings16(nextActions).slice(0, 6),
23510
+ receiptActions: uniqueStrings16(receiptActions).slice(0, 4),
23511
+ caveats
23512
+ };
23513
+ }
23295
23514
  function buildAgenticWorkStatus(cwd = process.cwd()) {
23296
23515
  const state = readWorkflowState2(cwd);
23297
23516
  const git = readLocalGitState(cwd);
@@ -23302,6 +23521,7 @@ function buildAgenticWorkStatus(cwd = process.cwd()) {
23302
23521
  const currentPhase = state ? currentWorkflowPhase(state) : void 0;
23303
23522
  const lastPhaseCommit = lastCompletedWorkflowPhase(state);
23304
23523
  const dirtyFileCount = git.statusLines?.length ?? 0;
23524
+ const pendingDecisionCount = decisionPendingCount(cwd);
23305
23525
  const risks = buildAgenticStatusRisks({
23306
23526
  state,
23307
23527
  dirtyFileCount,
@@ -23358,8 +23578,17 @@ function buildAgenticWorkStatus(cwd = process.cwd()) {
23358
23578
  },
23359
23579
  risks,
23360
23580
  openDecisions: {
23581
+ count: pendingDecisionCount,
23361
23582
  note: "Run snipara-companion brief for hosted decisions and memory authority signals."
23362
23583
  },
23584
+ operationalLoop: buildAgenticOperationalLoop({
23585
+ state,
23586
+ dirtyFileCount,
23587
+ risks,
23588
+ pendingDecisionCount,
23589
+ teamSyncSummary,
23590
+ latestHandoff
23591
+ }),
23363
23592
  suggestedNextAction: buildSuggestedAgenticNextAction(state, risks)
23364
23593
  };
23365
23594
  }
@@ -23418,8 +23647,24 @@ function printAgenticWorkStatus(status) {
23418
23647
  }
23419
23648
  console.log("");
23420
23649
  console.log(import_chalk6.default.bold("Open Decisions"));
23650
+ if (typeof status.openDecisions.count === "number") {
23651
+ console.log(`Local pending Decision Requests: ${status.openDecisions.count}`);
23652
+ }
23421
23653
  console.log(status.openDecisions.note);
23422
23654
  console.log("");
23655
+ console.log(import_chalk6.default.bold("Operational Loop"));
23656
+ console.log(`Status: ${status.operationalLoop.status}`);
23657
+ console.log(`Receipt gaps: ${status.operationalLoop.receiptGapCount}`);
23658
+ for (const action of status.operationalLoop.nextActions) {
23659
+ console.log(`- ${action}`);
23660
+ }
23661
+ for (const action of status.operationalLoop.receiptActions) {
23662
+ console.log(`- ${action}`);
23663
+ }
23664
+ for (const caveat of status.operationalLoop.caveats) {
23665
+ console.log(`- ${caveat}`);
23666
+ }
23667
+ console.log("");
23423
23668
  printKeyValue2("Next suggested action:", status.suggestedNextAction);
23424
23669
  if (status.workflow) {
23425
23670
  printKeyValue2("Resume:", status.workflow.resumeCommand);
@@ -34061,6 +34306,9 @@ workflow.command("scaffold").description("Generate a reusable managed workflow p
34061
34306
  workflow.command("decisions").description("List pending local decision requests for the LLM to ask the human").option("--json", "Print raw JSON").action(async (options) => {
34062
34307
  await workflowDecisionsCommand({ json: options.json });
34063
34308
  });
34309
+ workflow.command("policy-ledger").description("Summarize Project Policy decisions for agent-mediated review and audit").option("--json", "Print raw JSON").action(async (options) => {
34310
+ await workflowPolicyLedgerCommand({ json: options.json });
34311
+ });
34064
34312
  workflow.command("decide").description("Resolve a pending local decision request with an explicit human choice").argument("<request-id>", "Decision request id or fingerprint").requiredOption("--choose <option>", "Chosen option from the decision request").requiredOption("--reviewer <name>", "Human reviewer name or handle").option("--note <note>", "Review note").option("--json", "Print raw JSON").action(async (requestId, options) => {
34065
34313
  await workflowDecideCommand({
34066
34314
  requestId,
@@ -225,6 +225,7 @@ snipara-companion workflow phase-commit build --summary "tests green"
225
225
  snipara-companion workflow impact-gate
226
226
  snipara-companion workflow producer-triage
227
227
  snipara-companion workflow decisions
228
+ snipara-companion workflow policy-ledger
228
229
  snipara-companion workflow decide decision-abc123 --choose accept_all --reviewer alice
229
230
  snipara-companion workflow producer-report
230
231
  snipara-companion workflow producer-review --latest --outcome useful --reviewer alice
@@ -280,6 +281,10 @@ snipara-companion workflow resume --include-session-context
280
281
  client can render as a human question, including evidence, options,
281
282
  recommendation, declared apply path, and readable evidence items for batched
282
283
  decisions.
284
+ - `workflow policy-ledger` summarizes local Project Policy decision artifacts
285
+ for agent-mediated governance. It reports pending, approved, refused,
286
+ modified, deferred, and expired policy decisions, includes agent prompts for
287
+ pending human choices, and never applies or edits policy automatically.
283
288
  - `workflow decide <request-id> --choose <option> --reviewer <name>` writes a
284
289
  Decision Response receipt under `.snipara/decisions/resolved/`. For
285
290
  Producer Loop triage accept/reject choices, it applies the existing
@@ -1277,6 +1282,7 @@ Semantics:
1277
1282
  - `snipara-companion workflow phase-commit` = calls hosted `snipara_end_of_task_commit` for that phase, updates local state, and advances the next phase; if the hosted commit times out or hits a transient network failure, local workflow state still advances with an explicit local fallback record
1278
1283
  - `snipara-companion workflow phase-commit` and `workflow final-commit` also emit local Producer Loop artifacts under `.snipara/producer-loop/`, backed by the redacted Coding Intelligence Ledger. PR Answer Pack decision capture uses the same schema with producer kind `pr_answer_pack_decision_capture` when the artifact is exported or embedded by the hosted PR pack producer. These artifacts are review evidence only: they do not launch workers, approve durable memory, claim calibrated confidence, or provide server-side attestation.
1279
1284
  - `snipara-companion workflow decisions` = lists local pending Decision Request artifacts for LLM clients to ask the human, with evidence, options, recommendation, and apply-path metadata
1285
+ - `snipara-companion workflow policy-ledger` = read-only local Project Policy ledger for pending, approved, refused, modified, deferred, and expired policy decisions, including agent prompts for unresolved human choices
1280
1286
  - `snipara-companion workflow decide` = records a Decision Response receipt and moves the request to `.snipara/decisions/resolved/`; it never resolves by timeout/default, only applies existing reviewed paths such as `workflow producer-review`, and may emit review-only policy suggestion requests when repeated receipts show the same human rule
1281
1287
  - `snipara-companion workflow producer-triage` = emits a batched decision request for unreviewed Producer Loop samples; it does not mark samples reviewed until `workflow decide` records the human answer
1282
1288
  - `snipara-companion workflow producer-report` = scans local Producer Loop artifacts and reports adoption, producer kinds, workflow ids, latest artifact, reason-code counts, invalid artifacts, sample size, reviewed/rejected/unreviewed counts, and calibration caveats with `hardGateReady=false`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "3.2.15",
3
+ "version": "3.2.17",
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": {