portable-agent-layer 0.108.0 → 0.110.0

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.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The Relationship tab: the rule drafts waiting on the user, what the approved
3
3
  * rules did, the candidates still being proven, and the last 30 days of labelled
4
- * turns. A decision goes through `decideRule`, the same rule the CLI and the
5
- * rule-review skill use.
4
+ * turns. A decision goes through `decideRule` or `retireRule`, the same rules the
5
+ * CLI and the rule-review skill use.
6
6
  */
7
7
 
8
8
  import { type Candidate, readCandidates } from "../../hooks/lib/adaptation-candidates";
@@ -10,8 +10,16 @@ import {
10
10
  type AdaptationRule,
11
11
  decideRule,
12
12
  readRules,
13
+ retireRule,
14
+ type TriggerProof,
13
15
  } from "../../hooks/lib/adaptation-rules";
14
16
  import { readTurns, type Turn } from "../../hooks/lib/adaptation-turns";
17
+ import {
18
+ EFFECT_WINDOW_DAYS,
19
+ type OpinionEffect,
20
+ opinionEffectOf,
21
+ } from "../../hooks/lib/correction-rates";
22
+ import { gateAdvice } from "../../hooks/lib/opinion-gate";
15
23
  import {
16
24
  type RuleEffect,
17
25
  readRuleEvents,
@@ -25,12 +33,21 @@ const RECENT_CORRECTIONS = 10;
25
33
 
26
34
  export interface ActiveRule extends AdaptationRule {
27
35
  effect: RuleEffect;
36
+ opinionEffect: OpinionEffect | null;
28
37
  }
29
38
 
30
- export interface PipelineCandidate extends Candidate {
31
- ordinaryNeeded: number;
39
+ export interface DraftRule extends AdaptationRule {
40
+ /** Why it cannot be kept as an opinion, and what to do instead. */
41
+ gateAdvice?: string;
32
42
  }
33
43
 
44
+ export type PageDecision = "approved" | "denied" | "opinion" | "retired";
45
+
46
+ export type PipelineCandidate = Candidate & {
47
+ proof: TriggerProof;
48
+ ordinaryNeeded: number;
49
+ };
50
+
34
51
  interface TurnStats {
35
52
  total: number;
36
53
  byReaction: Record<string, number>;
@@ -40,7 +57,7 @@ interface TurnStats {
40
57
 
41
58
  export interface RelationshipView {
42
59
  windowDays: number;
43
- drafts: AdaptationRule[];
60
+ drafts: DraftRule[];
44
61
  active: ActiveRule[];
45
62
  denied: AdaptationRule[];
46
63
  pipeline: PipelineCandidate[];
@@ -69,33 +86,41 @@ export function relationship(now: Date = new Date()): RelationshipView {
69
86
  const rules = readRules();
70
87
  const turns = readTurns(WINDOW_DAYS, now);
71
88
  const effects = ruleEffects(readRuleEvents(WINDOW_DAYS, now), turns);
89
+ const opinionTurns = readTurns(EFFECT_WINDOW_DAYS, now);
72
90
  const withStatus = (status: AdaptationRule["status"]) =>
73
91
  rules.filter((rule) => rule.status === status);
74
92
  return {
75
93
  windowDays: WINDOW_DAYS,
76
- drafts: withStatus("draft"),
94
+ drafts: withStatus("draft").map((rule) => {
95
+ const advice = gateAdvice(rule, rules, opinionTurns, now);
96
+ return advice ? { ...rule, gateAdvice: advice } : rule;
97
+ }),
77
98
  active: withStatus("approved").map((rule) => ({
78
99
  ...rule,
79
100
  effect: effects[rule.id] ?? NO_EFFECT,
101
+ opinionEffect: opinionEffectOf(rule, rules, opinionTurns, now),
80
102
  })),
81
103
  denied: withStatus("denied"),
82
- pipeline: readCandidates()
83
- .filter((candidate) => candidate.verdict !== "passed")
84
- .map((candidate) => ({
85
- ...candidate,
86
- ordinaryNeeded:
87
- candidate.verdict === "waiting" ? ordinaryTurnsNeeded(candidate.proof) : 0,
88
- })),
104
+ pipeline: readCandidates().flatMap((candidate) =>
105
+ candidate.verdict === "passed" || candidate.verdict === "untriggered"
106
+ ? []
107
+ : [
108
+ {
109
+ ...candidate,
110
+ ordinaryNeeded:
111
+ candidate.verdict === "waiting"
112
+ ? ordinaryTurnsNeeded(candidate.proof)
113
+ : 0,
114
+ },
115
+ ]
116
+ ),
89
117
  turns: turnStats(turns),
90
118
  };
91
119
  }
92
120
 
93
- export function decideFromPage(
94
- id: string,
95
- decision: "approved" | "denied"
96
- ): WriteOutcome {
121
+ export function decideFromPage(id: string, decision: PageDecision): WriteOutcome {
97
122
  const known = readRules().some((rule) => rule.id === id);
98
- const result = decideRule(id, decision);
123
+ const result = decision === "retired" ? retireRule(id) : decideRule(id, decision);
99
124
  if (result.ok) return { ok: true, changed: true };
100
125
  return { ok: false, status: known ? 409 : 404, error: result.reason };
101
126
  }
@@ -18,7 +18,7 @@ import { agenda, agentsAtWork, board, handoffs, signal, summary } from "./data";
18
18
  import { projectDetail } from "./detail";
19
19
  import { matrix } from "./matrix";
20
20
  import { type PrefUpdate, readPrefs, validatePrefs, writePrefs } from "./prefs";
21
- import { decideFromPage, relationship } from "./relationship";
21
+ import { decideFromPage, type PageDecision, relationship } from "./relationship";
22
22
  import { DEFAULT_PORT, LOOPBACK } from "./server-config";
23
23
  import { MAX_SNOOZE_DAYS, setSnooze } from "./snooze";
24
24
  import { indexHtml, staticAsset } from "./static";
@@ -290,16 +290,23 @@ async function prefsWrite(request: Request): Promise<Response> {
290
290
  return failure ? json({ error: failure }, 400) : json(writePrefs(body as PrefUpdate));
291
291
  }
292
292
 
293
- /** Approve or deny a rule draft — the same rule `pal cli rule` reaches. */
293
+ const PAGE_DECISIONS: readonly PageDecision[] = [
294
+ "approved",
295
+ "denied",
296
+ "opinion",
297
+ "retired",
298
+ ];
299
+
300
+ /** Decide a rule draft or retire a rule — the same rules `pal cli rule` reaches. */
294
301
  async function ruleWrite(request: Request): Promise<Response> {
295
302
  const body = await readBody(request);
296
303
  if (!body) return json({ error: "expected a JSON body" }, 400);
297
304
  const { id, decision } = body;
298
305
  if (typeof id !== "string" || !id) return json({ error: "id is required" }, 400);
299
- if (decision !== "approved" && decision !== "denied") {
300
- return json({ error: "decision must be approved or denied" }, 400);
306
+ if (!PAGE_DECISIONS.includes(decision as PageDecision)) {
307
+ return json({ error: `decision must be one of ${PAGE_DECISIONS.join(", ")}` }, 400);
301
308
  }
302
- return answer(decideFromPage(id, decision), { id, decision });
309
+ return answer(decideFromPage(id, decision as PageDecision), { id, decision });
303
310
  }
304
311
 
305
312
  const WRITES: Record<string, (request: Request) => Promise<Response>> = {