@devspecai/pi-plugin 0.0.0-stage → 0.10.3

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +899 -0
  2. package/LICENSE +21 -0
  3. package/README.md +103 -2
  4. package/extensions/.gitkeep +0 -0
  5. package/extensions/core/activation-recovery.ts +135 -0
  6. package/extensions/core/active-deliveries.ts +171 -0
  7. package/extensions/core/active-session-plans.ts +374 -0
  8. package/extensions/core/adapter-overlap.ts +247 -0
  9. package/extensions/core/assistant-speech.ts +44 -0
  10. package/extensions/core/attach-context.ts +25 -0
  11. package/extensions/core/attachment-limits.ts +20 -0
  12. package/extensions/core/attachments.ts +487 -0
  13. package/extensions/core/auth-store.ts +91 -0
  14. package/extensions/core/automation-run-retry.ts +31 -0
  15. package/extensions/core/background-journal.ts +85 -0
  16. package/extensions/core/background-observer.ts +279 -0
  17. package/extensions/core/bond-recovery.ts +65 -0
  18. package/extensions/core/bond-state.ts +80 -0
  19. package/extensions/core/clear-conversation.ts +53 -0
  20. package/extensions/core/client-versions.ts +56 -0
  21. package/extensions/core/client.ts +704 -0
  22. package/extensions/core/command-offer.ts +88 -0
  23. package/extensions/core/command-runner.ts +62 -0
  24. package/extensions/core/command-turn.ts +88 -0
  25. package/extensions/core/commit-ledger.ts +195 -0
  26. package/extensions/core/commit-stamp.ts +98 -0
  27. package/extensions/core/config.ts +63 -0
  28. package/extensions/core/connection-context.ts +28 -0
  29. package/extensions/core/context-budget.ts +175 -0
  30. package/extensions/core/control-result-outbox.ts +80 -0
  31. package/extensions/core/control.ts +156 -0
  32. package/extensions/core/ctx-guard.ts +33 -0
  33. package/extensions/core/debug.ts +136 -0
  34. package/extensions/core/delivery-queue.ts +92 -0
  35. package/extensions/core/dispatch.ts +108 -0
  36. package/extensions/core/duck.ts +128 -0
  37. package/extensions/core/git-observer.ts +382 -0
  38. package/extensions/core/github-identity.ts +26 -0
  39. package/extensions/core/implementation-contract.ts +773 -0
  40. package/extensions/core/instructions.ts +40 -0
  41. package/extensions/core/interaction-events.ts +834 -0
  42. package/extensions/core/json-schema.ts +16 -0
  43. package/extensions/core/local-activity.ts +34 -0
  44. package/extensions/core/mcp-config-guard.ts +117 -0
  45. package/extensions/core/model-catalog.ts +76 -0
  46. package/extensions/core/model-surface.ts +125 -0
  47. package/extensions/core/notice-reply.ts +215 -0
  48. package/extensions/core/owner-inject.ts +148 -0
  49. package/extensions/core/post-session-receipt.ts +70 -0
  50. package/extensions/core/preempt.ts +100 -0
  51. package/extensions/core/project-choice.ts +145 -0
  52. package/extensions/core/project-scope.ts +335 -0
  53. package/extensions/core/project-selection-ui.ts +202 -0
  54. package/extensions/core/provenance-refs.ts +158 -0
  55. package/extensions/core/push-channel.ts +281 -0
  56. package/extensions/core/question-receipt-lifecycle.ts +158 -0
  57. package/extensions/core/recall.ts +121 -0
  58. package/extensions/core/remote-payload.ts +1329 -0
  59. package/extensions/core/remote-session-arg.ts +24 -0
  60. package/extensions/core/repository-context.ts +35 -0
  61. package/extensions/core/request-chain.ts +1244 -0
  62. package/extensions/core/result-projection.ts +939 -0
  63. package/extensions/core/result.ts +100 -0
  64. package/extensions/core/review-context.ts +70 -0
  65. package/extensions/core/review-watch.ts +94 -0
  66. package/extensions/core/room-access.ts +12 -0
  67. package/extensions/core/room-context.ts +178 -0
  68. package/extensions/core/room-reader.ts +308 -0
  69. package/extensions/core/room-store.ts +381 -0
  70. package/extensions/core/room-sync.ts +79 -0
  71. package/extensions/core/room-tools.ts +86 -0
  72. package/extensions/core/room-unread.ts +173 -0
  73. package/extensions/core/session-events.ts +62 -0
  74. package/extensions/core/session-polls.ts +292 -0
  75. package/extensions/core/settings.ts +39 -0
  76. package/extensions/core/status-line.ts +432 -0
  77. package/extensions/core/still-to-discuss.ts +124 -0
  78. package/extensions/core/subagent-fleet.ts +238 -0
  79. package/extensions/core/system-notice.ts +227 -0
  80. package/extensions/core/telemetry.ts +177 -0
  81. package/extensions/core/tool-selection.ts +72 -0
  82. package/extensions/core/trail-events.ts +247 -0
  83. package/extensions/core/trail-projection.ts +373 -0
  84. package/extensions/core/trail.ts +465 -0
  85. package/extensions/core/turn-lifecycle.ts +472 -0
  86. package/extensions/core/version-mismatch.ts +165 -0
  87. package/extensions/core/work-prompt.ts +44 -0
  88. package/extensions/core/work-state.ts +179 -0
  89. package/extensions/devspec-adapter-guard.ts +106 -0
  90. package/extensions/devspec-auth.ts +191 -0
  91. package/extensions/devspec-brand.ts +140 -0
  92. package/extensions/devspec-commit-ledger.ts +477 -0
  93. package/extensions/devspec-context.ts +146 -0
  94. package/extensions/devspec-memory.ts +136 -0
  95. package/extensions/devspec-provenance-assistance.ts +233 -0
  96. package/extensions/devspec-remote.ts +5152 -0
  97. package/extensions/devspec-tools.ts +552 -0
  98. package/extensions/devspec-work.ts +244 -0
  99. package/extensions/generated/README.md +18 -0
  100. package/extensions/generated/mcp-catalog.ts +15811 -0
  101. package/package.json +71 -4
  102. package/prompts/.gitkeep +0 -0
  103. package/runtime/commit-stamp.mjs +279 -0
  104. package/runtime/git-observer-shim.mjs +390 -0
  105. package/skills/.gitkeep +0 -0
  106. package/themes/.gitkeep +0 -0
@@ -0,0 +1,72 @@
1
+ import { TOOL_CATALOG_VERSION, MCP_CATALOG_FINGERPRINT } from '../generated/mcp-catalog.ts';
2
+ import { catalogEntry, exactModelToolNames, rankRareModelTools, TOOL_LOADER_NAME, toolSearchLimit, toolSearchPage, type ToolSearchPage } from './model-surface.ts';
3
+
4
+ type SelectionFailure = 'catalog_mismatch' | 'index_outdated' | 'embedding_unavailable' | 'invalid_response' | 'transport_unavailable';
5
+ const failureMessages: Record<SelectionFailure, string> = {
6
+ catalog_mismatch: 'Pi and DevSpec have different tool definitions. Update the extension with pi update --extensions and restart Pi; using local keyword search. Direct tools remain usable.',
7
+ index_outdated: 'DevSpec\'s meaning-based tool-search index is not ready; using local keyword search. Direct tools remain usable.',
8
+ embedding_unavailable: 'DevSpec could not rank tools by meaning; using local keyword search. Direct tools remain usable.',
9
+ invalid_response: 'DevSpec returned a tool-search response Pi could not read; using local keyword search. Direct tools remain usable.',
10
+ transport_unavailable: 'Pi\'s request to DevSpec tool search failed; using local keyword search. Direct tools remain usable.',
11
+ };
12
+
13
+ export interface SelectionResult {
14
+ mode: 'exact' | 'hybrid' | 'lexical';
15
+ degraded: boolean;
16
+ reason?: string;
17
+ failureReason?: SelectionFailure;
18
+ matches: Array<{ name: string; description: string }>;
19
+ page: ToolSearchPage;
20
+ unavailable: string[];
21
+ }
22
+
23
+ function serverPage(value: unknown): ToolSearchPage | null {
24
+ if (!value || typeof value !== 'object') return null;
25
+ const page = value as Record<string, unknown>;
26
+ const whole = (n: unknown) => typeof n === 'number' && Number.isInteger(n) && n >= 0;
27
+ if (!whole(page.total_matching) || !whole(page.offset) || !whole(page.limit) || !whole(page.returned) || typeof page.has_more !== 'boolean') return null;
28
+ const next = page.next_offset;
29
+ if (next !== null && !whole(next)) return null;
30
+ return { total_matching: page.total_matching as number, offset: page.offset as number, limit: page.limit as number, returned: page.returned as number, has_more: page.has_more, next_offset: next as number | null };
31
+ }
32
+
33
+ /** Never activates or executes a tool. Responses can only recommend locally
34
+ * registered, currently callable names, using this installation's own guidance.
35
+ */
36
+ export async function selectTools(query: string, limitInput: unknown, activeNames: readonly string[], request: (args: Record<string, unknown>) => Promise<unknown>, offsetInput: unknown = 0): Promise<SelectionResult> {
37
+ const limit = toolSearchLimit(limitInput);
38
+ const offset = typeof offsetInput === 'number' && Number.isFinite(offsetInput) ? Math.max(0, Math.trunc(offsetInput)) : 0;
39
+ const available = activeNames.filter(name => name !== TOOL_LOADER_NAME && catalogEntry(name)?.kind === 'model');
40
+ const known = new Set(available);
41
+ const exact = exactModelToolNames(query);
42
+ const local = () => {
43
+ const ranked = rankRareModelTools(query, available);
44
+ const matches = ranked.slice(offset, offset + limit).map(entry => ({ name: entry.name, description: entry.description }));
45
+ return { matches, page: toolSearchPage(ranked.length, offset, limit, matches.length) };
46
+ };
47
+ const fallback = (failureReason: SelectionFailure): SelectionResult => ({ mode: 'lexical', degraded: true, failureReason, reason: failureMessages[failureReason], ...local(), unavailable: [] });
48
+ if (exact) return { mode: 'exact', degraded: false, ...local(), unavailable: exact.filter(name => !known.has(name)) };
49
+ try {
50
+ const raw = await request({ query, limit, offset, available_tools: available, catalog_version: TOOL_CATALOG_VERSION, schema_fingerprint: MCP_CATALOG_FINGERPRINT });
51
+ const result = raw as { catalog_version?: unknown; mode?: unknown; degraded?: unknown; reason?: unknown; matches?: unknown; page?: unknown } | null;
52
+ if (result?.degraded === true) {
53
+ switch (result.reason) {
54
+ case 'catalog_mismatch': case 'index_outdated': case 'embedding_unavailable': return fallback(result.reason);
55
+ default: return fallback('invalid_response');
56
+ }
57
+ }
58
+ if (typeof result?.catalog_version === 'string' && result.catalog_version !== TOOL_CATALOG_VERSION) return fallback('catalog_mismatch');
59
+ const page = serverPage(result?.page);
60
+ if (!result || !page || result.catalog_version !== TOOL_CATALOG_VERSION || result.degraded !== false || result.mode !== 'hybrid' || !Array.isArray(result.matches) || result.matches.some(row => !row || typeof row !== 'object' || typeof (row as { name?: unknown }).name !== 'string')) {
61
+ return fallback('invalid_response');
62
+ }
63
+ const matches = result.matches.flatMap((row: unknown) => {
64
+ const name = row && typeof row === 'object' ? (row as { name?: unknown }).name : undefined;
65
+ const entry = typeof name === 'string' && known.has(name) ? catalogEntry(name) : undefined;
66
+ return entry ? [{ name: entry.name, description: entry.description }] : [];
67
+ });
68
+ return { mode: 'hybrid', degraded: false, matches: [...new Map(matches.map(hit => [hit.name, hit])).values()].slice(0, limit), page, unavailable: [] };
69
+ } catch {
70
+ return fallback('transport_unavailable');
71
+ }
72
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Structured work-trail events for Pi (item 2219b653).
3
+ *
4
+ * Pure capture + classify. The markdown renderer in trail.ts stays as the
5
+ * fallback for hosts that only speak `work_trail` text. Pi posts BOTH.
6
+ */
7
+
8
+ import { safeTrailId, safeTrailLabel } from "./trail-projection.ts";
9
+
10
+ export const TRAIL_EVENT_KINDS = ["thinking", "text", "tool", "domain", "subagent"] as const;
11
+ export type TrailEventKind = (typeof TRAIL_EVENT_KINDS)[number];
12
+
13
+ export const TRAIL_EVENT_STATUSES = ["running", "ok", "error"] as const;
14
+ export type TrailEventStatus = (typeof TRAIL_EVENT_STATUSES)[number];
15
+
16
+ export const TRAIL_DOMAINS = [
17
+ "task",
18
+ "memory",
19
+ "implementation",
20
+ "criterion",
21
+ "resource",
22
+ ] as const;
23
+ export type TrailDomain = (typeof TRAIL_DOMAINS)[number];
24
+
25
+ export interface ThinkingTrailEvent {
26
+ seq: number;
27
+ kind: "thinking";
28
+ text: string;
29
+ occurredAt: string;
30
+ }
31
+
32
+ /** Legacy only. Pi no longer emits assistant speech into Activity. */
33
+ export interface TextTrailEvent {
34
+ seq: number;
35
+ kind: "text";
36
+ text: string;
37
+ occurredAt?: string;
38
+ }
39
+
40
+ interface TimedTrailEvent {
41
+ occurredAt: string;
42
+ completedAt?: string;
43
+ }
44
+
45
+ export interface ToolTrailEvent extends TimedTrailEvent {
46
+ seq: number;
47
+ kind: "tool";
48
+ correlationId: string;
49
+ name: string;
50
+ args?: string;
51
+ status: TrailEventStatus;
52
+ result?: string;
53
+ durationMs?: number;
54
+ }
55
+
56
+ export interface DomainTrailEvent extends TimedTrailEvent {
57
+ seq: number;
58
+ kind: "domain";
59
+ correlationId: string;
60
+ domain: TrailDomain;
61
+ action: string;
62
+ name: string;
63
+ title?: string;
64
+ id?: string;
65
+ status: TrailEventStatus;
66
+ result?: string;
67
+ durationMs?: number;
68
+ }
69
+
70
+ export interface SubagentTrailChild {
71
+ key: string;
72
+ agent: string;
73
+ status: "pending" | "running" | "completed" | "failed" | "detached" | "stopped" | "paused";
74
+ model?: string;
75
+ thinking?: string;
76
+ lastActivityAt?: string;
77
+ durationMs?: number;
78
+ summary?: string;
79
+ }
80
+
81
+ export interface SubagentTrailEvent extends TimedTrailEvent {
82
+ seq: number;
83
+ kind: "subagent";
84
+ /** Intentionally omitted for public fleet-backed events; child/run ids stay local. */
85
+ correlationId?: string;
86
+ name: string;
87
+ status: TrailEventStatus;
88
+ summary?: string;
89
+ children: SubagentTrailChild[];
90
+ durationMs?: number;
91
+ }
92
+
93
+ export type TrailEvent =
94
+ | ThinkingTrailEvent
95
+ | TextTrailEvent
96
+ | ToolTrailEvent
97
+ | DomainTrailEvent
98
+ | SubagentTrailEvent;
99
+
100
+ interface DomainSpec {
101
+ domain: TrailDomain;
102
+ action: string;
103
+ }
104
+
105
+ const DOMAIN_BY_NAME: Record<string, DomainSpec> = {
106
+ create_task: { domain: "task", action: "created" },
107
+ update_task: { domain: "task", action: "updated" },
108
+ claim_work_item: { domain: "task", action: "claimed" },
109
+ record_implementation: { domain: "implementation", action: "recorded" },
110
+ record_criterion_verdicts: { domain: "criterion", action: "recorded" },
111
+ // The replacing item, like supersede_memory's successor: the one to read now.
112
+ supersede_task: { domain: "task", action: "superseded" },
113
+ record_memory: { domain: "memory", action: "recorded" },
114
+ update_memory: { domain: "memory", action: "updated" },
115
+ resolve_memory_flag: { domain: "memory", action: "reviewed" },
116
+ resolve_resource_flag: { domain: "resource", action: "reviewed" },
117
+ supersede_memory: { domain: "memory", action: "superseded" },
118
+ retract_memory: { domain: "memory", action: "retracted" },
119
+ create_resource: { domain: "resource", action: "created" },
120
+ update_resource: { domain: "resource", action: "updated" },
121
+ supersede_resource: { domain: "resource", action: "superseded" },
122
+ archive_resource: { domain: "resource", action: "archived" },
123
+ };
124
+
125
+ function asRecord(value: unknown): Record<string, unknown> | null {
126
+ return value && typeof value === "object" && !Array.isArray(value)
127
+ ? (value as Record<string, unknown>)
128
+ : null;
129
+ }
130
+
131
+ function pickString(obj: Record<string, unknown> | null, keys: string[]): string | undefined {
132
+ if (!obj) return undefined;
133
+ for (const key of keys) {
134
+ const v = obj[key];
135
+ if (typeof v === "string" && v.trim()) return v.trim();
136
+ }
137
+ return undefined;
138
+ }
139
+
140
+ function unwrapResult(result: unknown): Record<string, unknown> | null {
141
+ const outer = asRecord(result);
142
+ if (!outer) return null;
143
+ // Pi tool results retain the complete parsed payload in host-only `details`.
144
+ // Select identity fields mechanically, but never serialize the object.
145
+ const details = asRecord(outer.details);
146
+ if (details) return details;
147
+ const content = outer.content;
148
+ if (Array.isArray(content)) {
149
+ const text = content
150
+ .filter((c) => c && typeof c === "object" && (c as { type?: string }).type === "text")
151
+ .map((c) => (c as { text?: string }).text)
152
+ .filter((t): t is string => typeof t === "string")
153
+ .join("\n");
154
+ if (text.trim().startsWith("{")) {
155
+ try {
156
+ const parsed = JSON.parse(text) as unknown;
157
+ return asRecord(parsed);
158
+ } catch {
159
+ /* fall through */
160
+ }
161
+ }
162
+ }
163
+ return outer;
164
+ }
165
+
166
+ /** Map a native tool name to a first-class domain event. */
167
+ export function classifyDomainTool(name: string, _args?: unknown): DomainSpec | null {
168
+ return DOMAIN_BY_NAME[name] ?? null;
169
+ }
170
+
171
+ /** Fixed public dispositions only; receipt bodies and arbitrary outcome strings stay private. */
172
+ export type TrailWriteOutcome = "committed" | "queued_for_review" | "pending" | "not_applied" | "failed";
173
+
174
+ /** The boolean receipts a write sets: false means it did not apply, true that it did. */
175
+ const WRITE_MARKERS = [
176
+ "applied", "committed", "recorded", "created", "updated", "superseded",
177
+ "retracted", "archived", "claim_success", "implementation_recorded",
178
+ ] as const;
179
+
180
+ export function writeOutcomeFromResult(result: unknown): TrailWriteOutcome | undefined {
181
+ const row = unwrapResult(result);
182
+ if (!row) return undefined;
183
+ // Native toolErrorResult returns normally with details.error, not an isError flag.
184
+ // Classify the structured marker without projecting the private message.
185
+ if (typeof row.error === "string") return "failed";
186
+ if (asRecord(result)?.isError === true || row.isError === true || row.success === false || row.ok === false
187
+ || row.outcome === "failed" || row.outcome === "error") return "failed";
188
+ // Queued creates explicitly say recorded/created=false. Keep the proposal
189
+ // distinct from a write that did not apply or failed, without borrowing its source ID.
190
+ if (row.outcome === "queued_for_review") return "queued_for_review";
191
+ if (row.closure === "pending" || row.tense === "pending") return "pending";
192
+ if (WRITE_MARKERS.some((key) => row[key] === false)) return "not_applied";
193
+ if (row.outcome === "committed" || (row.outcome === "duplicate" && row.original_outcome === "committed")) return "committed";
194
+ // A new explicit disposition is not evidence that the default write happened.
195
+ if (typeof row.outcome === "string" && row.outcome.trim()) return "not_applied";
196
+ if (WRITE_MARKERS.some((key) => row[key] === true)) return "committed";
197
+ return undefined;
198
+ }
199
+
200
+ /**
201
+ * What the trail says a write DID. `DOMAIN_BY_NAME` names the committed outcome,
202
+ * because that is the ordinary one; a write the server only queued did not resolve,
203
+ * record or supersede anything, and saying it did is the failure the proposals
204
+ * ledger exists to prevent (DevSpec item 689d44cb).
205
+ */
206
+ export function domainAction(spec: DomainSpec, outcome?: TrailWriteOutcome): string {
207
+ if (outcome === undefined) return "outcome unknown";
208
+ if (outcome === "pending") return "pending";
209
+ if (outcome === "not_applied") return "not applied";
210
+ if (outcome === "failed") return "failed";
211
+ return outcome === "queued_for_review" ? "proposed" : spec.action;
212
+ }
213
+
214
+ export function domainIdentity(name: string, args?: unknown, result?: unknown): { id?: string; title?: string } {
215
+ const spec = DOMAIN_BY_NAME[name];
216
+ if (!spec) return {};
217
+ const a = asRecord(args);
218
+ const r = unwrapResult(result);
219
+ const item = asRecord(r?.item);
220
+ let nested: Record<string, unknown> | null = null;
221
+ if (spec.domain === "task") nested = asRecord(r?.task) ?? asRecord(item?.task) ?? item ?? r;
222
+ if (spec.domain === "memory") nested = asRecord(r?.memory) ?? asRecord(item?.memory) ?? item ?? r;
223
+ if (spec.domain === "resource") nested = asRecord(r?.resource) ?? asRecord(item?.resource) ?? item ?? r;
224
+ if (["implementation", "criterion"].includes(spec.domain)) {
225
+ nested = asRecord(r?.task) ?? asRecord(item?.task) ?? item ?? r;
226
+ }
227
+
228
+ const resultIdKeys = spec.domain === "memory"
229
+ ? ["id", "memory_id"]
230
+ : spec.domain === "resource"
231
+ ? ["id", "resource_id"]
232
+ : ["id", "task_id", "superseding_item_id"];
233
+ const argIdKeys = spec.domain === "memory"
234
+ ? ["memory_id", "id"]
235
+ : spec.domain === "resource"
236
+ ? ["resource_id", "id"]
237
+ : ["task_id", "id"];
238
+ const title = safeTrailLabel(pickString(nested, ["title"]) ?? pickString(a, ["title"]), 240);
239
+ const outcome = writeOutcomeFromResult(result);
240
+ if (result !== undefined && outcome !== "committed") return { title };
241
+ // Creates and supersedes mint a new identity. A missing replacement ID must
242
+ // never fall back to the old row merely because the host reported success.
243
+ const mintsIdentity = ["create_task", "record_memory", "create_resource", "supersede_memory", "supersede_resource"].includes(name);
244
+ const fallbackId = result !== undefined && mintsIdentity ? undefined : pickString(a, argIdKeys);
245
+ const id = safeTrailId(pickString(nested, resultIdKeys) ?? fallbackId);
246
+ return { id, title };
247
+ }