pmx-canvas 0.1.36 → 0.2.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.
- package/CHANGELOG.md +409 -0
- package/Readme.md +2 -2
- package/dist/json-render/index.js +89 -334
- package/dist/types/mcp/canvas-access.d.ts +5 -171
- package/dist/types/server/ax-state-manager.d.ts +256 -0
- package/dist/types/server/ax-state.d.ts +1 -1
- package/dist/types/server/canvas-operations.d.ts +1 -12
- package/dist/types/server/canvas-state.d.ts +3 -23
- package/dist/types/server/index.d.ts +6 -24
- package/dist/types/server/operations/composites.d.ts +121 -0
- package/dist/types/server/operations/http.d.ts +7 -0
- package/dist/types/server/operations/index.d.ts +8 -0
- package/dist/types/server/operations/invoker.d.ts +13 -0
- package/dist/types/server/operations/mcp.d.ts +15 -0
- package/dist/types/server/operations/ops/annotation.d.ts +2 -0
- package/dist/types/server/operations/ops/app.d.ts +33 -0
- package/dist/types/server/operations/ops/ax-await.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +31 -0
- package/dist/types/server/operations/ops/ax-state.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-timeline.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-work.d.ts +2 -0
- package/dist/types/server/operations/ops/batch.d.ts +19 -0
- package/dist/types/server/operations/ops/edges.d.ts +2 -0
- package/dist/types/server/operations/ops/groups.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +31 -0
- package/dist/types/server/operations/ops/nodes.d.ts +62 -0
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/ops/snapshots.d.ts +2 -0
- package/dist/types/server/operations/ops/validate.d.ts +2 -0
- package/dist/types/server/operations/ops/viewport.d.ts +2 -0
- package/dist/types/server/operations/ops/webview.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +15 -0
- package/dist/types/server/operations/types.d.ts +116 -0
- package/dist/types/server/operations/webview-runner.d.ts +69 -0
- package/docs/RELEASE.md +5 -0
- package/docs/adr-001-bun-only-runtime.md +46 -0
- package/docs/api-stability.md +57 -0
- package/docs/ax-state-contract.md +72 -0
- package/docs/mcp.md +60 -11
- package/docs/plans/plan-005-operation-registry.md +84 -0
- package/docs/plans/plan-006-mcp-tool-consolidation.md +109 -0
- package/docs/plans/plan-007-ax-domain.md +99 -0
- package/docs/plans/plan-008-registry-finish.md +91 -0
- package/docs/tech-debt-assessment-2026-06.md +90 -0
- package/package.json +3 -3
- package/skills/pmx-canvas/SKILL.md +192 -186
- package/skills/pmx-canvas/evals/evals.json +3 -3
- package/skills/pmx-canvas/references/codex-app-adapter.md +13 -14
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +4 -5
- package/src/cli/agent.ts +52 -31
- package/src/mcp/canvas-access.ts +30 -830
- package/src/mcp/server.ts +162 -2014
- package/src/server/ax-state-manager.ts +808 -0
- package/src/server/ax-state.ts +2 -2
- package/src/server/canvas-operations.ts +2 -328
- package/src/server/canvas-schema.ts +2 -2
- package/src/server/canvas-state.ts +95 -465
- package/src/server/index.ts +54 -190
- package/src/server/operations/composites.ts +355 -0
- package/src/server/operations/http.ts +103 -0
- package/src/server/operations/index.ts +65 -0
- package/src/server/operations/invoker.ts +87 -0
- package/src/server/operations/mcp.ts +221 -0
- package/src/server/operations/ops/annotation.ts +60 -0
- package/src/server/operations/ops/app.ts +447 -0
- package/src/server/operations/ops/ax-await.ts +216 -0
- package/src/server/operations/ops/ax-shared.ts +38 -0
- package/src/server/operations/ops/ax-state.ts +249 -0
- package/src/server/operations/ops/ax-timeline.ts +381 -0
- package/src/server/operations/ops/ax-work.ts +635 -0
- package/src/server/operations/ops/batch.ts +365 -0
- package/src/server/operations/ops/edges.ts +166 -0
- package/src/server/operations/ops/groups.ts +176 -0
- package/src/server/operations/ops/json-render.ts +691 -0
- package/src/server/operations/ops/nodes.ts +1047 -0
- package/src/server/operations/ops/query.ts +281 -0
- package/src/server/operations/ops/snapshots.ts +366 -0
- package/src/server/operations/ops/validate.ts +37 -0
- package/src/server/operations/ops/viewport.ts +219 -0
- package/src/server/operations/ops/webview.ts +339 -0
- package/src/server/operations/registry.ts +79 -0
- package/src/server/operations/types.ts +150 -0
- package/src/server/operations/webview-runner.ts +77 -0
- package/src/server/server.ts +158 -2255
- package/src/server/web-artifacts.ts +6 -2
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AX state manager.
|
|
3
|
+
*
|
|
4
|
+
* Owns the agent-experience (AX) state that previously lived inline in
|
|
5
|
+
* `CanvasStateManager`. Splits cleanly into the three documented partitions
|
|
6
|
+
* (see `docs/ax-state-contract.md`):
|
|
7
|
+
*
|
|
8
|
+
* • Canvas-bound (`_axState`): focus, work items, approval gates, review
|
|
9
|
+
* annotations, elicitations, mode requests, policy. Snapshotted, cleared by
|
|
10
|
+
* `canvas_clear`, replaced by `restore`. Mutators record undo/redo history
|
|
11
|
+
* (via the injected `recordMutation` / `suppressed` callbacks).
|
|
12
|
+
* • Timeline (audit-only): agent events, evidence, steering. DB-direct, NOT in
|
|
13
|
+
* `_axState`, NOT history-recorded, NOT snapshotted, retention-bounded.
|
|
14
|
+
* • Host/session: a single host-capability row in its own table.
|
|
15
|
+
*
|
|
16
|
+
* `CanvasStateManager` holds one of these and DELEGATES its public AX methods to
|
|
17
|
+
* it, so the SDK/HTTP/MCP surface is byte-stable. The manager takes injected
|
|
18
|
+
* callbacks for everything it does not own: a node-id-validity provider (used by
|
|
19
|
+
* normalization on write), the live DB handle, `scheduleSave`, `notifyChange`,
|
|
20
|
+
* `recordMutation`, and a `suppressed` wrapper for history closures.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
appendAxEventToDB,
|
|
25
|
+
appendAxEvidenceToDB,
|
|
26
|
+
appendAxSteeringToDB,
|
|
27
|
+
markAxSteeringDeliveredInDB,
|
|
28
|
+
loadAxEventsFromDB,
|
|
29
|
+
loadAxEvidenceFromDB,
|
|
30
|
+
loadAxSteeringFromDB,
|
|
31
|
+
loadPendingAxSteeringFromDB,
|
|
32
|
+
loadAxTimelineSummaryFromDB,
|
|
33
|
+
upsertAxHostCapabilityToDB,
|
|
34
|
+
loadAxHostCapabilityFromDB,
|
|
35
|
+
type AxTimelineQuery,
|
|
36
|
+
} from './canvas-db.js';
|
|
37
|
+
import {
|
|
38
|
+
createEmptyAxState,
|
|
39
|
+
createEmptyAxHostCapability,
|
|
40
|
+
normalizeAxState,
|
|
41
|
+
normalizeAxHostCapability,
|
|
42
|
+
createAxWorkItem,
|
|
43
|
+
createAxApprovalGate,
|
|
44
|
+
createAxReviewAnnotation,
|
|
45
|
+
createAxEvent,
|
|
46
|
+
createAxEvidence,
|
|
47
|
+
createAxSteeringMessage,
|
|
48
|
+
createAxElicitation,
|
|
49
|
+
createAxModeRequest,
|
|
50
|
+
isAxCommand,
|
|
51
|
+
listAxCommands,
|
|
52
|
+
AX_COMMAND_REGISTRY,
|
|
53
|
+
normalizeAxPolicy,
|
|
54
|
+
mapAxActivityKindToEventKind,
|
|
55
|
+
type PmxAxActivityKind,
|
|
56
|
+
type PmxAxElicitation,
|
|
57
|
+
type PmxAxModeRequest,
|
|
58
|
+
type PmxAxMode,
|
|
59
|
+
type PmxAxCommandDescriptor,
|
|
60
|
+
type PmxAxPolicy,
|
|
61
|
+
type PmxAxFocusState,
|
|
62
|
+
type PmxAxSource,
|
|
63
|
+
type PmxAxState,
|
|
64
|
+
type PmxAxWorkItem,
|
|
65
|
+
type PmxAxWorkItemStatus,
|
|
66
|
+
type PmxAxApprovalGate,
|
|
67
|
+
type PmxAxReviewAnnotation,
|
|
68
|
+
type PmxAxReviewKind,
|
|
69
|
+
type PmxAxReviewSeverity,
|
|
70
|
+
type PmxAxReviewStatus,
|
|
71
|
+
type PmxAxReviewAnchorType,
|
|
72
|
+
type PmxAxReviewRegion,
|
|
73
|
+
type PmxAxEvent,
|
|
74
|
+
type PmxAxEventKind,
|
|
75
|
+
type PmxAxEvidence,
|
|
76
|
+
type PmxAxEvidenceKind,
|
|
77
|
+
type PmxAxSteeringMessage,
|
|
78
|
+
type PmxAxHostCapability,
|
|
79
|
+
type PmxAxTimelineSummary,
|
|
80
|
+
} from './ax-state.js';
|
|
81
|
+
import type { CanvasChangeType, MutationRecordInfo } from './canvas-state.js';
|
|
82
|
+
|
|
83
|
+
type Database = import('bun:sqlite').Database;
|
|
84
|
+
|
|
85
|
+
/** Host-environment hooks the AX manager needs from its owner (CanvasStateManager). */
|
|
86
|
+
export interface AxStateManagerDeps {
|
|
87
|
+
/** Current valid node-id set — used by normalization on write to prune dangling refs. */
|
|
88
|
+
getNodeIds(): Set<string>;
|
|
89
|
+
/** Live DB handle for the timeline tables / host-capability table (null when no workspace). */
|
|
90
|
+
getDb(): Database | null;
|
|
91
|
+
/** Debounced save of the canvas-bound blob (timeline ops do NOT trigger this — they are DB-direct). */
|
|
92
|
+
scheduleSave(): void;
|
|
93
|
+
/** Emit a change notification (drives MCP resource notifications + blocking-wait endpoints). */
|
|
94
|
+
notifyChange(type: CanvasChangeType): void;
|
|
95
|
+
/** Record an undo/redo history entry. */
|
|
96
|
+
recordMutation(info: MutationRecordInfo): void;
|
|
97
|
+
/** Wrap a closure so it runs with mutation recording suppressed (for undo/redo replay). */
|
|
98
|
+
suppressed(fn: () => void): () => void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function replaceById<T extends { id: string }>(list: T[], item: T): T[] {
|
|
102
|
+
const idx = list.findIndex((x) => x.id === item.id);
|
|
103
|
+
if (idx === -1) return [...list, item];
|
|
104
|
+
const copy = list.slice();
|
|
105
|
+
copy[idx] = item;
|
|
106
|
+
return copy;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
110
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function logAxStateWarning(action: string, error: unknown, details?: Record<string, unknown>): void {
|
|
114
|
+
console.warn(`[ax-state] ${action}`, { error, ...(details ?? {}) });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class AxStateManager {
|
|
118
|
+
private _axState: PmxAxState = createEmptyAxState();
|
|
119
|
+
private _axHostCapability: PmxAxHostCapability | null = null;
|
|
120
|
+
|
|
121
|
+
constructor(private readonly deps: AxStateManagerDeps) {}
|
|
122
|
+
|
|
123
|
+
// ── Normalization (against the owner's current node set) ──────────
|
|
124
|
+
private normalizeForCurrentNodes(state: unknown): PmxAxState {
|
|
125
|
+
return normalizeAxState(state, this.deps.getNodeIds());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private applyAxState(state: PmxAxState): void {
|
|
129
|
+
this._axState = this.normalizeForCurrentNodes(state);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Load / snapshot / clear integration (called by CanvasStateManager) ──
|
|
133
|
+
|
|
134
|
+
/** Reset the canvas-bound partition to empty (used by clear() and applyPersistedState()). */
|
|
135
|
+
resetCanvasBound(): void {
|
|
136
|
+
this._axState = createEmptyAxState();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Replace the canvas-bound partition from a persisted/restored blob, normalized against current nodes. */
|
|
140
|
+
applyPersistedAx(ax: unknown): void {
|
|
141
|
+
this._axState = this.normalizeForCurrentNodes(ax);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Load the host-capability row from its own table (own partition; not snapshotted). */
|
|
145
|
+
loadHostCapabilityFromDb(): void {
|
|
146
|
+
const db = this.deps.getDb();
|
|
147
|
+
if (!db) return;
|
|
148
|
+
try {
|
|
149
|
+
this._axHostCapability = loadAxHostCapabilityFromDB(db);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logAxStateWarning('load host capability failed', error, {});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Re-normalize the canvas-bound partition against the current node set after a
|
|
157
|
+
* node was removed, and report what the removal orphaned. Work items / approval
|
|
158
|
+
* gates / elicitations / mode requests keep the item but strip the dangling node
|
|
159
|
+
* id ("re-anchored"); node-anchored review annotations are dropped ("removed").
|
|
160
|
+
* Returns the affected ids so the owner can record one audit timeline event.
|
|
161
|
+
*/
|
|
162
|
+
revalidateAfterNodeRemoval(removedNodeId: string): {
|
|
163
|
+
reanchoredIds: string[];
|
|
164
|
+
removedReviewIds: string[];
|
|
165
|
+
reanchoredFocus: boolean;
|
|
166
|
+
} {
|
|
167
|
+
const before = this._axState;
|
|
168
|
+
const referencedNode = (ids: string[]): boolean => ids.includes(removedNodeId);
|
|
169
|
+
const reanchoredIds: string[] = [];
|
|
170
|
+
for (const w of before.workItems) if (referencedNode(w.nodeIds)) reanchoredIds.push(w.id);
|
|
171
|
+
for (const g of before.approvalGates) if (referencedNode(g.nodeIds)) reanchoredIds.push(g.id);
|
|
172
|
+
for (const e of before.elicitations) if (referencedNode(e.nodeIds)) reanchoredIds.push(e.id);
|
|
173
|
+
for (const m of before.modeRequests) if (referencedNode(m.nodeIds)) reanchoredIds.push(m.id);
|
|
174
|
+
const removedReviewIds = before.reviewAnnotations
|
|
175
|
+
.filter((r) => r.anchorType === 'node' && r.nodeId === removedNodeId)
|
|
176
|
+
.map((r) => r.id);
|
|
177
|
+
// Focus is re-anchored too (the dangling id is normalized out) — reported so
|
|
178
|
+
// the audit note does not undercount what the deletion changed.
|
|
179
|
+
const reanchoredFocus = before.focus.nodeIds.includes(removedNodeId);
|
|
180
|
+
|
|
181
|
+
// The actual re-normalization (strips dangling refs, drops node-anchored reviews).
|
|
182
|
+
this.applyAxState(before);
|
|
183
|
+
|
|
184
|
+
return { reanchoredIds, removedReviewIds, reanchoredFocus };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Canvas-bound readers ──────────────────────────────────────────
|
|
188
|
+
getAxState(): PmxAxState {
|
|
189
|
+
return structuredClone(this.normalizeForCurrentNodes(this._axState));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getAxFocus(): PmxAxFocusState {
|
|
193
|
+
return this.getAxState().focus;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
setAxFocus(nodeIds: string[], options: { source?: PmxAxSource; recordHistory?: boolean } = {}): PmxAxFocusState {
|
|
197
|
+
const oldAxState = this.getAxState();
|
|
198
|
+
const nextAxState: PmxAxState = {
|
|
199
|
+
...oldAxState,
|
|
200
|
+
focus: {
|
|
201
|
+
nodeIds,
|
|
202
|
+
primaryNodeId: nodeIds[0] ?? null,
|
|
203
|
+
updatedAt: new Date().toISOString(),
|
|
204
|
+
source: options.source ?? 'api',
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
this.applyAxState(nextAxState);
|
|
208
|
+
const appliedAxState = this.getAxState();
|
|
209
|
+
this.deps.scheduleSave();
|
|
210
|
+
this.deps.notifyChange('ax');
|
|
211
|
+
if (options.recordHistory === false) return appliedAxState.focus;
|
|
212
|
+
this.deps.recordMutation({
|
|
213
|
+
operationType: 'setAxFocus',
|
|
214
|
+
description: `Set AX focus (${appliedAxState.focus.nodeIds.length} nodes)`,
|
|
215
|
+
forward: this.deps.suppressed(() => {
|
|
216
|
+
this.applyAxState(appliedAxState);
|
|
217
|
+
this.deps.scheduleSave();
|
|
218
|
+
this.deps.notifyChange('ax');
|
|
219
|
+
}),
|
|
220
|
+
inverse: this.deps.suppressed(() => {
|
|
221
|
+
this.applyAxState(oldAxState);
|
|
222
|
+
this.deps.scheduleSave();
|
|
223
|
+
this.deps.notifyChange('ax');
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
return appliedAxState.focus;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
clearAxFocus(): PmxAxFocusState {
|
|
230
|
+
return this.setAxFocus([], { source: 'system' });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Work items (canvas-bound; snapshotted via getAxState blob) ────
|
|
234
|
+
getWorkItems(): PmxAxWorkItem[] {
|
|
235
|
+
return this.getAxState().workItems;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
addWorkItem(
|
|
239
|
+
input: { title: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
|
|
240
|
+
options: { source?: PmxAxSource } = {},
|
|
241
|
+
): PmxAxWorkItem {
|
|
242
|
+
const oldAxState = this.getAxState();
|
|
243
|
+
const item = createAxWorkItem(input, options.source ?? 'api', this.deps.getNodeIds());
|
|
244
|
+
this.applyAxState({ ...oldAxState, workItems: [...oldAxState.workItems, item] });
|
|
245
|
+
const applied = this.getAxState();
|
|
246
|
+
this.deps.scheduleSave();
|
|
247
|
+
this.deps.notifyChange('ax');
|
|
248
|
+
this.deps.recordMutation({
|
|
249
|
+
operationType: 'addWorkItem',
|
|
250
|
+
description: `Added work item "${item.title}"`,
|
|
251
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
252
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
253
|
+
});
|
|
254
|
+
return applied.workItems.find((w) => w.id === item.id) ?? item;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
updateWorkItem(
|
|
258
|
+
id: string,
|
|
259
|
+
patch: { title?: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
|
|
260
|
+
options: { source?: PmxAxSource } = {},
|
|
261
|
+
): PmxAxWorkItem | null {
|
|
262
|
+
const oldAxState = this.getAxState();
|
|
263
|
+
const existing = oldAxState.workItems.find((w) => w.id === id);
|
|
264
|
+
if (!existing) return null;
|
|
265
|
+
const validNodeIds = this.deps.getNodeIds();
|
|
266
|
+
const merged: PmxAxWorkItem = {
|
|
267
|
+
...existing,
|
|
268
|
+
...(patch.title !== undefined ? { title: patch.title } : {}),
|
|
269
|
+
...(patch.status !== undefined ? { status: patch.status } : {}),
|
|
270
|
+
...(patch.detail !== undefined ? { detail: patch.detail } : {}),
|
|
271
|
+
...(patch.nodeIds !== undefined ? { nodeIds: patch.nodeIds.filter((n) => validNodeIds.has(n)) } : {}),
|
|
272
|
+
updatedAt: new Date().toISOString(),
|
|
273
|
+
source: options.source ?? existing.source,
|
|
274
|
+
};
|
|
275
|
+
this.applyAxState({ ...oldAxState, workItems: replaceById(oldAxState.workItems, merged) });
|
|
276
|
+
const applied = this.getAxState();
|
|
277
|
+
this.deps.scheduleSave();
|
|
278
|
+
this.deps.notifyChange('ax');
|
|
279
|
+
this.deps.recordMutation({
|
|
280
|
+
operationType: 'updateWorkItem',
|
|
281
|
+
description: `Updated work item ${id}`,
|
|
282
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
283
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
284
|
+
});
|
|
285
|
+
return applied.workItems.find((w) => w.id === id) ?? null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Approval gates (canvas-bound) ─────────────────────────────────
|
|
289
|
+
getApprovalGates(): PmxAxApprovalGate[] {
|
|
290
|
+
return this.getAxState().approvalGates;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
requestApproval(
|
|
294
|
+
input: { title: string; detail?: string | null; action?: string | null; nodeIds?: string[] },
|
|
295
|
+
options: { source?: PmxAxSource } = {},
|
|
296
|
+
): PmxAxApprovalGate {
|
|
297
|
+
const oldAxState = this.getAxState();
|
|
298
|
+
const gate = createAxApprovalGate(input, options.source ?? 'api', this.deps.getNodeIds());
|
|
299
|
+
this.applyAxState({ ...oldAxState, approvalGates: [...oldAxState.approvalGates, gate] });
|
|
300
|
+
const applied = this.getAxState();
|
|
301
|
+
this.deps.scheduleSave();
|
|
302
|
+
this.deps.notifyChange('ax');
|
|
303
|
+
this.deps.recordMutation({
|
|
304
|
+
operationType: 'requestApproval',
|
|
305
|
+
description: `Requested approval "${gate.title}"`,
|
|
306
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
307
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
308
|
+
});
|
|
309
|
+
return applied.approvalGates.find((g) => g.id === gate.id) ?? gate;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
resolveApproval(
|
|
313
|
+
id: string,
|
|
314
|
+
decision: 'approved' | 'rejected',
|
|
315
|
+
options: { resolution?: string; source?: PmxAxSource } = {},
|
|
316
|
+
): PmxAxApprovalGate | null {
|
|
317
|
+
const oldAxState = this.getAxState();
|
|
318
|
+
const gate = oldAxState.approvalGates.find((g) => g.id === id);
|
|
319
|
+
if (!gate || gate.status !== 'pending') return null;
|
|
320
|
+
const resolved: PmxAxApprovalGate = {
|
|
321
|
+
...gate,
|
|
322
|
+
status: decision,
|
|
323
|
+
resolvedAt: new Date().toISOString(),
|
|
324
|
+
resolution: options.resolution ?? null,
|
|
325
|
+
source: options.source ?? gate.source,
|
|
326
|
+
};
|
|
327
|
+
this.applyAxState({ ...oldAxState, approvalGates: replaceById(oldAxState.approvalGates, resolved) });
|
|
328
|
+
const applied = this.getAxState();
|
|
329
|
+
this.deps.scheduleSave();
|
|
330
|
+
this.deps.notifyChange('ax');
|
|
331
|
+
this.deps.recordMutation({
|
|
332
|
+
operationType: 'resolveApproval',
|
|
333
|
+
description: `Resolved approval ${id} -> ${decision}`,
|
|
334
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
335
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
336
|
+
});
|
|
337
|
+
return applied.approvalGates.find((g) => g.id === id) ?? null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Review annotations (canvas-bound) ─────────────────────────────
|
|
341
|
+
getReviewAnnotations(): PmxAxReviewAnnotation[] {
|
|
342
|
+
return this.getAxState().reviewAnnotations;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
addReviewAnnotation(
|
|
346
|
+
input: {
|
|
347
|
+
body: string;
|
|
348
|
+
kind?: PmxAxReviewKind;
|
|
349
|
+
severity?: PmxAxReviewSeverity;
|
|
350
|
+
anchorType?: PmxAxReviewAnchorType;
|
|
351
|
+
nodeId?: string | null;
|
|
352
|
+
file?: string | null;
|
|
353
|
+
region?: PmxAxReviewRegion | null;
|
|
354
|
+
author?: string | null;
|
|
355
|
+
},
|
|
356
|
+
options: { source?: PmxAxSource } = {},
|
|
357
|
+
): PmxAxReviewAnnotation | null {
|
|
358
|
+
// Validate the node anchor up front. A node-anchored review whose nodeId is
|
|
359
|
+
// missing or unknown would otherwise be silently dropped by
|
|
360
|
+
// normalizeForCurrentNodes after apply, yet still returned as a phantom
|
|
361
|
+
// success object — false success / silent data loss. Reject instead so the
|
|
362
|
+
// HTTP/MCP layers surface ok:false / 4xx.
|
|
363
|
+
// Context-aware default: only fall back to a node anchor when a usable nodeId
|
|
364
|
+
// is present; otherwise treat it as an unanchored (body-only) note so a
|
|
365
|
+
// `{ body }`-only annotation succeeds (anchorType is documented optional).
|
|
366
|
+
const anchorType = input.anchorType ?? (typeof input.nodeId === 'string' && input.nodeId ? 'node' : 'file');
|
|
367
|
+
// An EXPLICIT node anchor still requires a real nodeId — reject a phantom
|
|
368
|
+
// node-anchored review rather than silently dropping it post-apply.
|
|
369
|
+
if (anchorType === 'node' && (typeof input.nodeId !== 'string' || !this.deps.getNodeIds().has(input.nodeId))) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const oldAxState = this.getAxState();
|
|
373
|
+
const annotation = createAxReviewAnnotation(input, options.source ?? 'api');
|
|
374
|
+
this.applyAxState({ ...oldAxState, reviewAnnotations: [...oldAxState.reviewAnnotations, annotation] });
|
|
375
|
+
const applied = this.getAxState();
|
|
376
|
+
this.deps.scheduleSave();
|
|
377
|
+
this.deps.notifyChange('ax');
|
|
378
|
+
this.deps.recordMutation({
|
|
379
|
+
operationType: 'addReviewAnnotation',
|
|
380
|
+
description: `Added review ${annotation.kind} (${annotation.severity})`,
|
|
381
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
382
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
383
|
+
});
|
|
384
|
+
return applied.reviewAnnotations.find((r) => r.id === annotation.id) ?? annotation;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
updateReviewAnnotation(
|
|
388
|
+
id: string,
|
|
389
|
+
patch: { body?: string; status?: PmxAxReviewStatus; severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind },
|
|
390
|
+
options: { source?: PmxAxSource } = {},
|
|
391
|
+
): PmxAxReviewAnnotation | null {
|
|
392
|
+
const oldAxState = this.getAxState();
|
|
393
|
+
const existing = oldAxState.reviewAnnotations.find((r) => r.id === id);
|
|
394
|
+
if (!existing) return null;
|
|
395
|
+
const merged: PmxAxReviewAnnotation = {
|
|
396
|
+
...existing,
|
|
397
|
+
...(patch.body !== undefined ? { body: patch.body } : {}),
|
|
398
|
+
...(patch.status !== undefined ? { status: patch.status } : {}),
|
|
399
|
+
...(patch.severity !== undefined ? { severity: patch.severity } : {}),
|
|
400
|
+
...(patch.kind !== undefined ? { kind: patch.kind } : {}),
|
|
401
|
+
updatedAt: new Date().toISOString(),
|
|
402
|
+
source: options.source ?? existing.source,
|
|
403
|
+
};
|
|
404
|
+
this.applyAxState({ ...oldAxState, reviewAnnotations: replaceById(oldAxState.reviewAnnotations, merged) });
|
|
405
|
+
const applied = this.getAxState();
|
|
406
|
+
this.deps.scheduleSave();
|
|
407
|
+
this.deps.notifyChange('ax');
|
|
408
|
+
this.deps.recordMutation({
|
|
409
|
+
operationType: 'updateReviewAnnotation',
|
|
410
|
+
description: `Updated review ${id}`,
|
|
411
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
412
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
413
|
+
});
|
|
414
|
+
return applied.reviewAnnotations.find((r) => r.id === id) ?? null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ── Host capability (own table; reported by adapters) ─────────────
|
|
418
|
+
getHostCapability(): PmxAxHostCapability | null {
|
|
419
|
+
return this._axHostCapability;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Elicitations (canvas-bound) ───────────────────────────────────
|
|
423
|
+
getElicitations(): PmxAxElicitation[] {
|
|
424
|
+
return this.getAxState().elicitations;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
requestElicitation(
|
|
428
|
+
input: { prompt: string; fields?: string[]; nodeIds?: string[] },
|
|
429
|
+
options: { source?: PmxAxSource } = {},
|
|
430
|
+
): PmxAxElicitation {
|
|
431
|
+
const oldAxState = this.getAxState();
|
|
432
|
+
const elicitation = createAxElicitation(input, options.source ?? 'api', this.deps.getNodeIds());
|
|
433
|
+
this.applyAxState({ ...oldAxState, elicitations: [...oldAxState.elicitations, elicitation] });
|
|
434
|
+
const applied = this.getAxState();
|
|
435
|
+
this.deps.scheduleSave();
|
|
436
|
+
this.deps.notifyChange('ax');
|
|
437
|
+
this.deps.recordMutation({
|
|
438
|
+
operationType: 'requestElicitation',
|
|
439
|
+
description: `Requested elicitation "${elicitation.prompt}"`,
|
|
440
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
441
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
442
|
+
});
|
|
443
|
+
return applied.elicitations.find((e) => e.id === elicitation.id) ?? elicitation;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
respondElicitation(
|
|
447
|
+
id: string,
|
|
448
|
+
response: Record<string, unknown>,
|
|
449
|
+
options: { source?: PmxAxSource } = {},
|
|
450
|
+
): PmxAxElicitation | null {
|
|
451
|
+
const oldAxState = this.getAxState();
|
|
452
|
+
const existing = oldAxState.elicitations.find((e) => e.id === id);
|
|
453
|
+
if (!existing || existing.status !== 'pending') return null;
|
|
454
|
+
const merged: PmxAxElicitation = {
|
|
455
|
+
...existing,
|
|
456
|
+
status: 'answered',
|
|
457
|
+
response,
|
|
458
|
+
resolvedAt: new Date().toISOString(),
|
|
459
|
+
source: options.source ?? existing.source,
|
|
460
|
+
};
|
|
461
|
+
this.applyAxState({ ...oldAxState, elicitations: replaceById(oldAxState.elicitations, merged) });
|
|
462
|
+
const applied = this.getAxState();
|
|
463
|
+
this.deps.scheduleSave();
|
|
464
|
+
this.deps.notifyChange('ax');
|
|
465
|
+
this.deps.recordMutation({
|
|
466
|
+
operationType: 'respondElicitation',
|
|
467
|
+
description: `Answered elicitation ${id}`,
|
|
468
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
469
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
470
|
+
});
|
|
471
|
+
return applied.elicitations.find((e) => e.id === id) ?? null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── Mode requests (canvas-bound) ──────────────────────────────────
|
|
475
|
+
getModeRequests(): PmxAxModeRequest[] {
|
|
476
|
+
return this.getAxState().modeRequests;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
requestMode(
|
|
480
|
+
input: { mode: PmxAxMode; reason?: string | null; nodeIds?: string[] },
|
|
481
|
+
options: { source?: PmxAxSource } = {},
|
|
482
|
+
): PmxAxModeRequest {
|
|
483
|
+
const oldAxState = this.getAxState();
|
|
484
|
+
const request = createAxModeRequest(input, options.source ?? 'api', this.deps.getNodeIds());
|
|
485
|
+
this.applyAxState({ ...oldAxState, modeRequests: [...oldAxState.modeRequests, request] });
|
|
486
|
+
const applied = this.getAxState();
|
|
487
|
+
this.deps.scheduleSave();
|
|
488
|
+
this.deps.notifyChange('ax');
|
|
489
|
+
this.deps.recordMutation({
|
|
490
|
+
operationType: 'requestMode',
|
|
491
|
+
description: `Requested mode "${request.mode}"`,
|
|
492
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
493
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
494
|
+
});
|
|
495
|
+
return applied.modeRequests.find((m) => m.id === request.id) ?? request;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
resolveModeRequest(
|
|
499
|
+
id: string,
|
|
500
|
+
decision: 'approved' | 'rejected',
|
|
501
|
+
options: { resolution?: string; source?: PmxAxSource } = {},
|
|
502
|
+
): PmxAxModeRequest | null {
|
|
503
|
+
const oldAxState = this.getAxState();
|
|
504
|
+
const existing = oldAxState.modeRequests.find((m) => m.id === id);
|
|
505
|
+
if (!existing || existing.status !== 'pending') return null;
|
|
506
|
+
const merged: PmxAxModeRequest = {
|
|
507
|
+
...existing,
|
|
508
|
+
status: decision,
|
|
509
|
+
resolvedAt: new Date().toISOString(),
|
|
510
|
+
resolution: options.resolution ?? null,
|
|
511
|
+
source: options.source ?? existing.source,
|
|
512
|
+
};
|
|
513
|
+
this.applyAxState({ ...oldAxState, modeRequests: replaceById(oldAxState.modeRequests, merged) });
|
|
514
|
+
const applied = this.getAxState();
|
|
515
|
+
this.deps.scheduleSave();
|
|
516
|
+
this.deps.notifyChange('ax');
|
|
517
|
+
this.deps.recordMutation({
|
|
518
|
+
operationType: 'resolveModeRequest',
|
|
519
|
+
description: `Resolved mode request ${id} -> ${decision}`,
|
|
520
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
521
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
522
|
+
});
|
|
523
|
+
return applied.modeRequests.find((m) => m.id === id) ?? null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ── Single-item AX readers (canvas-bound; for the blocking-wait endpoints) ──
|
|
527
|
+
getApproval(id: string): PmxAxApprovalGate | null {
|
|
528
|
+
return this.getAxState().approvalGates.find((g) => g.id === id) ?? null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
getElicitation(id: string): PmxAxElicitation | null {
|
|
532
|
+
return this.getAxState().elicitations.find((e) => e.id === id) ?? null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
getModeRequest(id: string): PmxAxModeRequest | null {
|
|
536
|
+
return this.getAxState().modeRequests.find((m) => m.id === id) ?? null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
getCommandRegistry(): PmxAxCommandDescriptor[] {
|
|
540
|
+
return listAxCommands();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Invoke a registry-gated PMX command intent — records a timeline event (no execution). */
|
|
544
|
+
invokeCommand(name: string, args: Record<string, unknown> | null = null, options: { source?: PmxAxSource } = {}): PmxAxEvent | null {
|
|
545
|
+
if (!isAxCommand(name)) return null;
|
|
546
|
+
return this.recordAxEvent(
|
|
547
|
+
{ kind: 'command', summary: name, detail: AX_COMMAND_REGISTRY[name].description, data: { command: name, ...(args ? { args } : {}) } },
|
|
548
|
+
options,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
getPolicy(): PmxAxPolicy {
|
|
553
|
+
return this.getAxState().policy;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Merge a declarative tool/prompt policy patch (canvas-bound, snapshotted). */
|
|
557
|
+
setPolicy(
|
|
558
|
+
patch: { tools?: Partial<PmxAxPolicy['tools']>; prompt?: Partial<PmxAxPolicy['prompt']> },
|
|
559
|
+
_options: { source?: PmxAxSource } = {},
|
|
560
|
+
): PmxAxPolicy {
|
|
561
|
+
const oldAxState = this.getAxState();
|
|
562
|
+
const merged = normalizeAxPolicy({
|
|
563
|
+
tools: { ...oldAxState.policy.tools, ...(patch.tools ?? {}) },
|
|
564
|
+
prompt: { ...oldAxState.policy.prompt, ...(patch.prompt ?? {}) },
|
|
565
|
+
});
|
|
566
|
+
this.applyAxState({ ...oldAxState, policy: merged });
|
|
567
|
+
const applied = this.getAxState();
|
|
568
|
+
this.deps.scheduleSave();
|
|
569
|
+
this.deps.notifyChange('ax');
|
|
570
|
+
this.deps.recordMutation({
|
|
571
|
+
operationType: 'setPolicy',
|
|
572
|
+
description: 'Updated AX policy',
|
|
573
|
+
forward: this.deps.suppressed(() => { this.applyAxState(applied); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
574
|
+
inverse: this.deps.suppressed(() => { this.applyAxState(oldAxState); this.deps.scheduleSave(); this.deps.notifyChange('ax'); }),
|
|
575
|
+
});
|
|
576
|
+
return applied.policy;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
setHostCapability(input: unknown, _options: { source?: PmxAxSource } = {}): PmxAxHostCapability {
|
|
580
|
+
const cap = normalizeAxHostCapability(
|
|
581
|
+
isRecord(input)
|
|
582
|
+
? { ...input, reportedAt: new Date().toISOString() }
|
|
583
|
+
: { reportedAt: new Date().toISOString() },
|
|
584
|
+
) ?? createEmptyAxHostCapability();
|
|
585
|
+
this._axHostCapability = cap;
|
|
586
|
+
const db = this.deps.getDb();
|
|
587
|
+
if (db) {
|
|
588
|
+
try {
|
|
589
|
+
upsertAxHostCapabilityToDB(db, cap);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
logAxStateWarning('save host capability failed', error, {});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
this.deps.notifyChange('ax');
|
|
595
|
+
return cap;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── Timeline (DB-direct; NOT in _axState; NOT history-recorded) ───
|
|
599
|
+
recordAxEvent(
|
|
600
|
+
input: { kind: PmxAxEventKind; summary: string; detail?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
|
|
601
|
+
options: { source?: PmxAxSource } = {},
|
|
602
|
+
): PmxAxEvent {
|
|
603
|
+
const draft = createAxEvent(input, options.source ?? 'api');
|
|
604
|
+
const db = this.deps.getDb();
|
|
605
|
+
if (db) {
|
|
606
|
+
try {
|
|
607
|
+
const ev = appendAxEventToDB(db, draft);
|
|
608
|
+
this.deps.notifyChange('ax-timeline');
|
|
609
|
+
return ev;
|
|
610
|
+
} catch (error) {
|
|
611
|
+
logAxStateWarning('record ax event failed', error, { id: draft.id });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
this.deps.notifyChange('ax-timeline');
|
|
615
|
+
return { ...draft, seq: 0 };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
addEvidence(
|
|
619
|
+
input: { kind: PmxAxEvidenceKind; title: string; body?: string | null; ref?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
|
|
620
|
+
options: { source?: PmxAxSource } = {},
|
|
621
|
+
): PmxAxEvidence {
|
|
622
|
+
const draft = createAxEvidence(input, options.source ?? 'api');
|
|
623
|
+
const db = this.deps.getDb();
|
|
624
|
+
if (db) {
|
|
625
|
+
try {
|
|
626
|
+
const ev = appendAxEvidenceToDB(db, draft);
|
|
627
|
+
this.deps.notifyChange('ax-timeline');
|
|
628
|
+
return ev;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
logAxStateWarning('add evidence failed', error, { id: draft.id });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
this.deps.notifyChange('ax-timeline');
|
|
634
|
+
return { ...draft, seq: 0 };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
recordSteeringMessage(message: string, options: { source?: PmxAxSource } = {}): PmxAxSteeringMessage {
|
|
638
|
+
const draft = createAxSteeringMessage(message, options.source ?? 'api');
|
|
639
|
+
const db = this.deps.getDb();
|
|
640
|
+
if (db) {
|
|
641
|
+
try {
|
|
642
|
+
const s = appendAxSteeringToDB(db, draft);
|
|
643
|
+
this.deps.notifyChange('ax-timeline');
|
|
644
|
+
return s;
|
|
645
|
+
} catch (error) {
|
|
646
|
+
logAxStateWarning('record steering failed', error, { id: draft.id });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
this.deps.notifyChange('ax-timeline');
|
|
650
|
+
return { ...draft, seq: 0 };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
markSteeringDelivered(id: string): boolean {
|
|
654
|
+
const db = this.deps.getDb();
|
|
655
|
+
if (!db) return false;
|
|
656
|
+
try {
|
|
657
|
+
const ok = markAxSteeringDeliveredInDB(db, id);
|
|
658
|
+
if (ok) this.deps.notifyChange('ax-timeline');
|
|
659
|
+
return ok;
|
|
660
|
+
} catch (error) {
|
|
661
|
+
logAxStateWarning('mark steering delivered failed', error, { id });
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Ingest a normalized agent activity (a tool/session event a harness forwards)
|
|
668
|
+
* and apply kind-driven board reactions, so the agent's real work flows back into
|
|
669
|
+
* the board without it remembering to push each item (report primitive A — makes
|
|
670
|
+
* AX bidirectional). Always records a timeline event; then, unless the caller
|
|
671
|
+
* overrides/suppresses via `reactions`, applies defaults by kind/outcome:
|
|
672
|
+
* • failure | error | outcome==='failure' → work item (blocked) + review
|
|
673
|
+
* (finding/error, anchored to a valid nodeId else the `ref` file) + evidence (logs)
|
|
674
|
+
* • tool-result + outcome==='success' → evidence (tool-result)
|
|
675
|
+
* • everything else (tool-start, session-*, command, note) → event only
|
|
676
|
+
* A reaction value of `false` suppresses it; an object overrides its fields/forces it on.
|
|
677
|
+
*/
|
|
678
|
+
ingestActivity(
|
|
679
|
+
input: {
|
|
680
|
+
kind: PmxAxActivityKind;
|
|
681
|
+
title: string;
|
|
682
|
+
summary?: string | null;
|
|
683
|
+
outcome?: 'success' | 'failure';
|
|
684
|
+
ref?: string | null;
|
|
685
|
+
nodeIds?: string[];
|
|
686
|
+
data?: Record<string, unknown> | null;
|
|
687
|
+
reactions?: {
|
|
688
|
+
workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
|
|
689
|
+
evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
|
|
690
|
+
review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
|
|
691
|
+
};
|
|
692
|
+
},
|
|
693
|
+
options: { source?: PmxAxSource } = {},
|
|
694
|
+
): { event: PmxAxEvent; workItem: PmxAxWorkItem | null; evidence: PmxAxEvidence | null; review: PmxAxReviewAnnotation | null } {
|
|
695
|
+
const source = options.source ?? 'api';
|
|
696
|
+
const summary = input.summary ?? input.title;
|
|
697
|
+
const isFailure = input.kind === 'failure' || input.kind === 'error' || input.outcome === 'failure';
|
|
698
|
+
const isToolSuccess = input.kind === 'tool-result' && input.outcome === 'success';
|
|
699
|
+
const nodeIds = input.nodeIds ?? [];
|
|
700
|
+
const validNodeIds = this.deps.getNodeIds();
|
|
701
|
+
const anchorNodeId = nodeIds.find((n) => validNodeIds.has(n)) ?? null;
|
|
702
|
+
|
|
703
|
+
// (1) Always record the activity on the timeline (precise kind on data.activityKind).
|
|
704
|
+
const event = this.recordAxEvent(
|
|
705
|
+
{
|
|
706
|
+
kind: mapAxActivityKindToEventKind(input.kind),
|
|
707
|
+
summary: input.title,
|
|
708
|
+
detail: input.summary ?? null,
|
|
709
|
+
nodeIds,
|
|
710
|
+
// Caller data first so the canonical fields always win — a malformed/hostile
|
|
711
|
+
// payload can't overwrite activityKind/outcome/ref (which the docstring +
|
|
712
|
+
// reaction logic treat as authoritative).
|
|
713
|
+
data: {
|
|
714
|
+
...(input.data ?? {}),
|
|
715
|
+
activityKind: input.kind,
|
|
716
|
+
...(input.outcome ? { outcome: input.outcome } : {}),
|
|
717
|
+
...(input.ref ? { ref: input.ref } : {}),
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
{ source },
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
// (2) Resolve reactions: kind-driven defaults, overridable per call.
|
|
724
|
+
const r = input.reactions ?? {};
|
|
725
|
+
const wantWorkItem = r.workItem === false ? null : (r.workItem ?? (isFailure ? {} : null));
|
|
726
|
+
const wantEvidence = r.evidence === false
|
|
727
|
+
? null
|
|
728
|
+
: (r.evidence ?? (isFailure ? { kind: 'logs' as PmxAxEvidenceKind } : isToolSuccess ? { kind: 'tool-result' as PmxAxEvidenceKind } : null));
|
|
729
|
+
const wantReview = r.review === false ? null : (r.review ?? (isFailure ? {} : null));
|
|
730
|
+
|
|
731
|
+
let workItem: PmxAxWorkItem | null = null;
|
|
732
|
+
if (wantWorkItem) {
|
|
733
|
+
workItem = this.addWorkItem(
|
|
734
|
+
{ title: input.title, status: wantWorkItem.status ?? 'blocked', detail: wantWorkItem.detail ?? summary, nodeIds },
|
|
735
|
+
{ source },
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
let evidence: PmxAxEvidence | null = null;
|
|
740
|
+
if (wantEvidence) {
|
|
741
|
+
evidence = this.addEvidence(
|
|
742
|
+
{ kind: wantEvidence.kind ?? 'logs', title: input.title, body: wantEvidence.body ?? input.summary ?? null, ref: input.ref ?? null, nodeIds },
|
|
743
|
+
{ source },
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
let review: PmxAxReviewAnnotation | null = null;
|
|
748
|
+
if (wantReview) {
|
|
749
|
+
const reviewNodeId = wantReview.nodeId ?? anchorNodeId;
|
|
750
|
+
// addReviewAnnotation returns null on a bad node anchor — that just skips the
|
|
751
|
+
// review; it never fails the whole ingest (the event + other reactions stand).
|
|
752
|
+
review = this.addReviewAnnotation(
|
|
753
|
+
{
|
|
754
|
+
body: summary,
|
|
755
|
+
kind: wantReview.kind ?? 'finding',
|
|
756
|
+
severity: wantReview.severity ?? 'error',
|
|
757
|
+
...(wantReview.anchorType ? { anchorType: wantReview.anchorType } : {}),
|
|
758
|
+
...(reviewNodeId ? { nodeId: reviewNodeId } : {}),
|
|
759
|
+
...(input.ref ? { file: input.ref } : {}),
|
|
760
|
+
},
|
|
761
|
+
{ source },
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return { event, workItem, evidence, review };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
getAxEvents(q: AxTimelineQuery = {}): PmxAxEvent[] {
|
|
769
|
+
const db = this.deps.getDb();
|
|
770
|
+
return db ? loadAxEventsFromDB(db, q) : [];
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
getAxEvidence(q: AxTimelineQuery = {}): PmxAxEvidence[] {
|
|
774
|
+
const db = this.deps.getDb();
|
|
775
|
+
return db ? loadAxEvidenceFromDB(db, q) : [];
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
getAxSteering(q: AxTimelineQuery & { onlyPending?: boolean } = {}): PmxAxSteeringMessage[] {
|
|
779
|
+
const db = this.deps.getDb();
|
|
780
|
+
return db ? loadAxSteeringFromDB(db, q) : [];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Undelivered steering for a consumer (Phase 4 delivery). Excludes messages
|
|
785
|
+
* whose source equals the consumer to prevent delivery loops (e.g. Copilot
|
|
786
|
+
* should not be handed back steering it originated).
|
|
787
|
+
*/
|
|
788
|
+
getPendingSteering(options: { consumer?: string; limit?: number } = {}): PmxAxSteeringMessage[] {
|
|
789
|
+
const db = this.deps.getDb();
|
|
790
|
+
return db ? loadPendingAxSteeringFromDB(db, options) : [];
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
getAxTimelineSummary(): PmxAxTimelineSummary {
|
|
794
|
+
const db = this.deps.getDb();
|
|
795
|
+
return db
|
|
796
|
+
? loadAxTimelineSummaryFromDB(db)
|
|
797
|
+
: { recentEvents: [], recentEvidence: [], pendingSteering: [], counts: { events: 0, evidence: 0, steering: 0 } };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
getAxTimeline(q: AxTimelineQuery = {}): { events: PmxAxEvent[]; evidence: PmxAxEvidence[]; steering: PmxAxSteeringMessage[]; summary: PmxAxTimelineSummary } {
|
|
801
|
+
return {
|
|
802
|
+
events: this.getAxEvents(q),
|
|
803
|
+
evidence: this.getAxEvidence(q),
|
|
804
|
+
steering: this.getAxSteering(q),
|
|
805
|
+
summary: this.getAxTimelineSummary(),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|