snipara-companion 3.2.16 → 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.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) {
@@ -34149,6 +34306,9 @@ workflow.command("scaffold").description("Generate a reusable managed workflow p
34149
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) => {
34150
34307
  await workflowDecisionsCommand({ json: options.json });
34151
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
+ });
34152
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) => {
34153
34313
  await workflowDecideCommand({
34154
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.16",
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": {