Skip to content

Commit 7e89ed0

Browse files
committed
release: prepare 0.3.0 explainable focus
1 parent e6b4fc5 commit 7e89ed0

12 files changed

Lines changed: 461 additions & 41 deletions

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ and agents attention now? actually sees to th
6464

6565
- a Focus surface inside Paperclip
6666
- ranked `now`, `next`, and `ambient` attention lanes
67+
- embedded explainability in the Focus UI, including `Why now`, `Why next`, confidence, signals, thread context, and related activity
6768
- approval handling, including budget-specific approval semantics
6869
- issue-aware operator language such as `review required`, `blocked`, and targeted recommended moves
6970
- agent-aware routing that distinguishes known company agents from human/operator roles when issue text references them
@@ -75,13 +76,31 @@ and agents attention now? actually sees to th
7576
- durable acknowledge/suppression behavior backed by plugin state and ledger replay
7677
- a sidebar entry, page, and dashboard widget
7778

79+
## Explainability
80+
81+
Focus is meant to be more legible than a smart inbox.
82+
83+
The first `0.3.0` explainability slice keeps reasoning attached to the cards you are already acting on:
84+
85+
- `Why now` on the active card explains why the current item outranks the rest of the queue
86+
- `Why next` on queued rows explains why something is staged behind the current top item
87+
- `Confidence` shows how strong the semantic signal is when the plugin has one
88+
- `Signals` surfaces the specific factors that pushed the item into attention
89+
- `Thread context` and `Related activity` help explain whether an item is part of a broader episode or continuation
90+
91+
The intent is not to expose every internal scoring detail. It is to help an operator answer:
92+
93+
- why did this surface?
94+
- why is it in this lane?
95+
- how much should I trust this judgment?
96+
7897
## Package Boundary
7998

8099
This plugin treats Paperclip as the host runtime and UI shell, while embedding [Aperture Core](https://github.com/tomismeta/aperture/tree/main/packages/core) through the npm package [`@tomismeta/aperture-core`](https://www.npmjs.com/package/@tomismeta/aperture-core).
81100

82101
It is a pure SDK integration: Aperture Core is used as-is inside a self-contained Paperclip plugin, with no changes to Aperture Core or Paperclip core.
83102

84-
For `0.2.x`, the boundary works like this:
103+
For `0.3.x`, the boundary works like this:
85104

86105
- the plugin worker owns Aperture ingestion, replay, review state, and display composition
87106
- Paperclip remains the system of record for issue and approval writes
@@ -129,6 +148,8 @@ Then open `http://127.0.0.1:3100/APE/aperture` and verify:
129148
- approval actions update Focus correctly
130149
- resolved blocker comments downgrade stale `Now` items
131150
- attached issue documents downgrade stale `share the memo/spec` review blockers into monitor-only follow-up
151+
- the active card exposes `Why now`
152+
- expanded queued rows expose `Why next`
132153

133154
## Links
134155

docs/RELEASE-0.3.0.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# paperclip-aperture 0.3.0
2+
3+
`0.3.0` turns Focus into a more explainable operator surface without changing the core live-attention workflow.
4+
5+
## Highlights
6+
7+
- Added embedded explainability to the Focus UI
8+
- Added `Why now` on the active card and `Why next` on queued rows
9+
- Exposed confidence, signals, thread context, and related activity in the card details
10+
- Persisted explainability metadata on reconciled issue, approval, and agent frames
11+
12+
## What Changed
13+
14+
- added a shared explainability layer that turns frame provenance, semantic confidence, relation hints, and episode continuity into operator-facing rationale
15+
- active `Now` cards now expose:
16+
- `Why now`
17+
- `Confidence`
18+
- `Signals`
19+
- `Thread context`
20+
- `Related activity`
21+
- expanded `Next` rows now show a compact `Why next` strip
22+
- explainability metadata is now attached to reconciled issue frames and approval bootstrap frames so the UI is reading worker-owned semantics rather than inventing its own rationale
23+
- copy was tightened to feel like product language rather than internal semantic-engine language
24+
25+
## Why This Matters
26+
27+
- Focus is more legible in place, without forcing operators into a separate inspection mode
28+
- operators can now answer:
29+
- why did this surface?
30+
- why is it in this lane?
31+
- how much should I trust this judgment?
32+
- the plugin keeps its existing architectural line:
33+
- Aperture Core remains the bounded semantic substrate
34+
- paperclip-aperture remains the Paperclip-specific interpreter and operator-language layer
35+
36+
## Validation
37+
38+
- `pnpm test`
39+
- `pnpm typecheck`
40+
- `pnpm build`
41+
- `pnpm release:check`
42+
- live-smoke-tested against a local Paperclip instance with screenshots of both:
43+
- the active `Why now` panel
44+
- the queued `Why next` strip

docs/ROADMAP.md

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Roadmap and Releasing
22

33
This document defines how `paperclip-aperture` should mature from a strong `0.1.x`
4-
integration into a dependable operator surface.
4+
integration into a dependable and legible operator surface.
55

66
The guiding principle is simple:
77

@@ -42,50 +42,60 @@ These should only happen if we find a clearly reusable SDK need:
4242
- structured "Why this?" trace export
4343
- standardized provenance and reasoning surfaces for host integrations
4444

45+
## Current Position
46+
47+
`0.2.x` largely delivered the trustworthy Focus foundation:
48+
49+
- dependable replay and review state
50+
- stronger issue, approval, and agent semantics
51+
- richer recommended moves
52+
- document-aware review downgrade behavior
53+
- a clearer `now`, `next`, and `ambient` model
54+
55+
The next wedge is no longer basic trust. It is explainability and operator control.
56+
4557
## Next Feature Set
4658

47-
The next meaningful release should focus on trust and operator usefulness, not just UI polish.
59+
The next meaningful release should focus on legibility and operator usefulness, not just ranking polish.
4860

49-
### 0.2.x: Trustworthy Focus
61+
### 0.3.0: Explainable Focus
5062

5163
Goals:
5264

53-
- make `Focus` dependable after restarts and missed events
54-
- improve non-approval coverage
55-
- reduce ambiguity about why something is `now`, `next`, or `ambient`
65+
- make `Focus` more transparent than a styled inbox
66+
- help operators understand why something is interrupting them or queued behind the active item
67+
- preserve the fast action surface while making judgment easier to trust
5668

5769
Scope:
5870

59-
- broader backfill/reconciliation beyond approvals
60-
- stronger blocked/waiting/run-failure semantics
61-
- deeper links back into Paperclip entities
62-
- richer operator actions for high-value frames
63-
- unread/review state or lightweight "new since last seen" model
71+
- embedded `Why now` and `Why next` surfaces
72+
- confidence, signals, thread context, and related-activity visibility
73+
- richer provenance UI that stays attached to the existing Focus cards
74+
- stronger lane-specific treatments without introducing a separate inspection mode first
6475

6576
Success criteria:
6677

67-
- approvals, issues, and failed runs all feel trustworthy after restart
68-
- operators can move from `Focus` directly into the underlying Paperclip work
69-
- a new operator can explain why a frame landed in a given lane
78+
- an operator can explain why the current item is `now`
79+
- an operator can explain why a queued item is `next`
80+
- explainability improves trust without slowing the action loop
7081

71-
### 0.3.x: Explainability and Review
82+
### 0.3.x: Operator Control and Review
7283

7384
Goals:
7485

75-
- make the plugin more transparent than a styled inbox
76-
- expose Aperture's reasoning in a way operators can trust
86+
- give operators more lifecycle control over attention once they trust the queue
87+
- reduce long-tail noise without weakening the core ranking model
7788

7889
Scope:
7990

80-
- `Why this?` surface
81-
- richer provenance UI
8291
- review/mute/snooze patterns
83-
- stronger lane-specific row treatments
92+
- stronger retirement behavior for ambient and stale tails
93+
- tighter review controls on queued and ambient items
8494

8595
Success criteria:
8696

87-
- an operator can inspect why a frame is interrupting them
88-
- the plugin supports both quick action and thoughtful review
97+
- operators can shape what stays visible over time
98+
- the plugin supports both quick action and deliberate queue management
8999

90100
## Release Checklist
91101

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tomismeta/paperclip-aperture",
3-
"version": "0.2.2",
3+
"version": "0.3.0",
44
"type": "module",
55
"private": false,
66
"description": "The live attention layer for Paperclip, powered by Aperture's deterministic attention engine.",

src/aperture/approval-frames.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export function approvalRecordToFrame(record: ApprovalRecord): StoredAttentionFr
5959
? payload.summary
6060
: approvalBlockingSummary(budgetOverride);
6161
const updatedAt = record.updatedAt ?? record.createdAt;
62+
const provenance = {
63+
whyNow: approvalBlockingWhyNow(budgetOverride),
64+
factors: budgetOverride
65+
? ["budget stop", "approval", "operator decision"]
66+
: ["approval", "operator decision"],
67+
};
6268

6369
return {
6470
id: `approval-bootstrap:${record.id}`,
@@ -95,19 +101,20 @@ export function approvalRecordToFrame(record: ApprovalRecord): StoredAttentionFr
95101
: []),
96102
],
97103
},
98-
provenance: {
99-
whyNow: approvalBlockingWhyNow(budgetOverride),
100-
factors: budgetOverride
101-
? ["budget stop", "approval", "operator decision"]
102-
: ["approval", "operator decision"],
103-
},
104+
provenance,
104105
timing: {
105106
createdAt: record.createdAt,
106107
updatedAt,
107108
},
108109
metadata: {
109110
approvalStatus: record.status,
110111
approvalType: record.type,
112+
attention: {
113+
rationale: provenance.factors,
114+
},
115+
semantic: {
116+
confidence: "high",
117+
},
111118
},
112119
};
113120
}

src/aperture/explainability.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import type { SemanticConfidence, SemanticRelationHint } from "@tomismeta/aperture-core/semantic";
2+
import type { FrameLane } from "./frame-model.js";
3+
import type { StoredAttentionFrame } from "./types.js";
4+
5+
export type FrameExplainability = {
6+
whyNow: string | null;
7+
laneReason: string;
8+
signalStrength: SemanticConfidence | null;
9+
signals: string[];
10+
relationLabels: string[];
11+
continuity: string | null;
12+
};
13+
14+
function asRecord(value: unknown): Record<string, unknown> | null {
15+
return typeof value === "object" && value !== null && !Array.isArray(value)
16+
? value as Record<string, unknown>
17+
: null;
18+
}
19+
20+
function readStringArray(value: unknown): string[] {
21+
if (!Array.isArray(value)) return [];
22+
return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
23+
}
24+
25+
function readSemanticConfidence(frame: StoredAttentionFrame): SemanticConfidence | null {
26+
const semantic = asRecord(frame.metadata?.semantic);
27+
const confidence = semantic?.confidence;
28+
return confidence === "low" || confidence === "medium" || confidence === "high" ? confidence : null;
29+
}
30+
31+
function readSemanticRelationHints(frame: StoredAttentionFrame): SemanticRelationHint[] {
32+
const semantic = asRecord(frame.metadata?.semantic);
33+
const relationHints = semantic?.relationHints;
34+
if (!Array.isArray(relationHints)) return [];
35+
36+
return relationHints.filter((entry): entry is SemanticRelationHint => {
37+
if (typeof entry !== "object" || entry === null) return false;
38+
const hint = entry as Record<string, unknown>;
39+
return (
40+
hint.kind === "same_issue"
41+
|| hint.kind === "resolves"
42+
|| hint.kind === "supersedes"
43+
|| hint.kind === "repeats"
44+
|| hint.kind === "escalates"
45+
);
46+
});
47+
}
48+
49+
function relationHintLabel(hint: SemanticRelationHint): string {
50+
switch (hint.kind) {
51+
case "same_issue":
52+
return "Part of the same thread";
53+
case "resolves":
54+
return "Resolves an earlier blocker";
55+
case "supersedes":
56+
return "Moves the request forward";
57+
case "repeats":
58+
return "Repeats an earlier ask";
59+
case "escalates":
60+
return "Raises the urgency";
61+
}
62+
}
63+
64+
function laneReason(lane: FrameLane): string {
65+
switch (lane) {
66+
case "active":
67+
return "This is the most urgent item in the queue right now.";
68+
case "queued":
69+
return "This is queued behind the current top item.";
70+
case "ambient":
71+
return "This is visible for awareness without needing action yet.";
72+
}
73+
}
74+
75+
function continuitySummary(frame: StoredAttentionFrame): string | null {
76+
const episode = asRecord(frame.metadata?.episode);
77+
const size = typeof episode?.size === "number" ? episode.size : null;
78+
const state = typeof episode?.state === "string" ? episode.state.replace(/_/g, " ") : null;
79+
80+
if (!size || size <= 1) return null;
81+
if (state) return `Part of a ${state} thread with ${size} related interactions.`;
82+
return `Part of a thread with ${size} related interactions.`;
83+
}
84+
85+
function frameSignals(frame: StoredAttentionFrame): string[] {
86+
const attention = asRecord(frame.metadata?.attention);
87+
const rationale = readStringArray(attention?.rationale);
88+
const factors = readStringArray(frame.provenance?.factors);
89+
return [...new Set([...(rationale.length > 0 ? rationale : factors), ...factors])];
90+
}
91+
92+
export function signalStrengthLabel(confidence: SemanticConfidence): string {
93+
return `${confidence} confidence`;
94+
}
95+
96+
export function explainFrame(frame: StoredAttentionFrame, lane: FrameLane): FrameExplainability {
97+
const relationLabels = [...new Set(readSemanticRelationHints(frame).map(relationHintLabel))];
98+
99+
return {
100+
whyNow: frame.provenance?.whyNow ?? frame.summary ?? null,
101+
laneReason: laneReason(lane),
102+
signalStrength: readSemanticConfidence(frame),
103+
signals: frameSignals(frame),
104+
relationLabels,
105+
continuity: continuitySummary(frame),
106+
};
107+
}

src/aperture/reconciliation.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Agent, Issue, IssueComment, PluginContext, PluginIssuesClient } from "@paperclipai/plugin-sdk";
2+
import type { SemanticConfidence } from "@tomismeta/aperture-core/semantic";
23
import type { AttentionReviewState, AttentionSnapshot, StoredAttentionFrame } from "./types.js";
34
import {
45
agentStatusItem,
@@ -35,6 +36,18 @@ type IssueDocumentSummary = Awaited<ReturnType<PluginIssuesClient["documents"]["
3536

3637
const COMMENT_LOOKUP_CONCURRENCY = 6;
3738

39+
function semanticMetadata(
40+
confidence: SemanticConfidence | null,
41+
relationHints: IssueIntentAnalysis["relationHints"],
42+
): Record<string, unknown> | undefined {
43+
const semantic = {
44+
...(confidence ? { confidence } : {}),
45+
...(relationHints.length > 0 ? { relationHints } : {}),
46+
};
47+
48+
return Object.keys(semantic).length > 0 ? semantic : undefined;
49+
}
50+
3851
function toIsoString(value: Date | string | null | undefined): string | undefined {
3952
if (!value) return undefined;
4053
if (typeof value === "string") return value;
@@ -135,6 +148,7 @@ function issueFrame(
135148
const owner = hasIntent(analysis, "resolution") || documentSignal.resolvesArtifactRequest ? null : analysis.owner;
136149
const move = issueRecommendedMove(issue, analysis, documentSignal);
137150
const target = analysis.blockingTarget;
151+
const semantic = semanticMetadata(analysis.semanticConfidence, analysis.relationHints);
138152

139153
return {
140154
lane,
@@ -175,6 +189,10 @@ function issueFrame(
175189
issuePriority: issue.priority,
176190
liveReconciled: true,
177191
activityPath: comment ? "activity" : undefined,
192+
attention: {
193+
rationale: provenance.factors ?? [],
194+
},
195+
...(semantic ? { semantic } : {}),
178196
},
179197
},
180198
};
@@ -249,6 +267,12 @@ function agentFrame(agent: Agent): StoredFrameCandidate | null {
249267
pauseReason: agent.pauseReason,
250268
liveReconciled: true,
251269
activityPath: agent.status === "error" ? "activity" : undefined,
270+
attention: {
271+
rationale: provenance.factors ?? [],
272+
},
273+
semantic: {
274+
confidence: "high" as const,
275+
},
252276
},
253277
},
254278
};

0 commit comments

Comments
 (0)