@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,244 @@
1
+ /**
2
+ * DevSpec Pi — explicit work selection and package help.
3
+ *
4
+ * /devspec.work [id-or-prefix]
5
+ * Arg: resolve one explicit item.
6
+ * No arg: present an item selector when UI is available. It never drains a
7
+ * queue; task work is selected directly and reserved by the agent.
8
+ *
9
+ * The command mechanically loads/caches the product implementation contract,
10
+ * records the delivery for the selected item, and sends one real user kickoff
11
+ * message. Claim guards remain server-authoritative. There is no execution mode
12
+ * to select — DevSpec item 6d085535 deleted the concept.
13
+ */
14
+
15
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
17
+ import { createHash } from "node:crypto";
18
+ import { getAdapterGuardState, SUPPRESSION_NOTICE } from "./core/adapter-overlap.ts";
19
+ import { markTypedSend, unmarkTypedSend } from "./core/request-chain.ts";
20
+ import { resolveToken } from "./core/auth-store.ts";
21
+ import { callDevspec, MODEL_TIMEOUT_MS } from "./core/client.ts";
22
+ import { agentsSettingsUrl } from "./core/config.ts";
23
+ import {
24
+ buildImplementationKickoff,
25
+ createImplementationContractLoader,
26
+ loadImplementationContext,
27
+ } from "./core/implementation-contract.ts";
28
+ import { recordActiveDelivery, rollbackActiveDelivery } from "./core/active-deliveries.ts";
29
+ import { unwrapMcpResult } from "./core/result.ts";
30
+ import { noteWork, paintStatus } from "./core/status-line.ts";
31
+ import { buildWorkPrompt, type WorkTarget } from "./core/work-prompt.ts";
32
+
33
+ const CACHE_TTL_MS = 60_000;
34
+
35
+ interface ItemSummary {
36
+ id: string;
37
+ title: string;
38
+ }
39
+
40
+ let cacheCwd = process.cwd();
41
+ let itemCache: { at: number; scope: string; items: ItemSummary[] } = { at: 0, scope: "", items: [] };
42
+
43
+ function itemCacheScope(token: string, cwd: string): string {
44
+ return `${createHash("sha256").update(token, "utf8").digest("hex").slice(0, 24)}:${cwd}`;
45
+ }
46
+
47
+ async function refreshItemCache(cwd = cacheCwd): Promise<boolean> {
48
+ const resolved = await resolveToken();
49
+ if (!resolved) {
50
+ itemCache = { at: 0, scope: "", items: [] };
51
+ return false;
52
+ }
53
+ const scope = itemCacheScope(resolved.token, cwd);
54
+ try {
55
+ const raw = await callDevspec(
56
+ resolved.token,
57
+ "get_tasks",
58
+ { fields: "compact", limit: 100 },
59
+ { lane: "work", timeoutMs: MODEL_TIMEOUT_MS },
60
+ );
61
+ const { details } = unwrapMcpResult(raw);
62
+ const rows = (details as { items?: Array<{ id?: string; title?: string }> } | null)?.items;
63
+ if (Array.isArray(rows)) {
64
+ itemCache = {
65
+ at: Date.now(),
66
+ scope,
67
+ items: rows
68
+ .filter((row) => typeof row?.id === "string")
69
+ .map((row) => ({ id: row.id as string, title: typeof row.title === "string" ? row.title : "" })),
70
+ };
71
+ return true;
72
+ }
73
+ return false;
74
+ } catch {
75
+ // Stale autocomplete is acceptable only within the same authority/cwd.
76
+ if (itemCache.scope !== scope) itemCache = { at: 0, scope, items: [] };
77
+ return false;
78
+ }
79
+ }
80
+
81
+ function refreshIfStale(): void {
82
+ if (Date.now() - itemCache.at > CACHE_TTL_MS) void refreshItemCache();
83
+ }
84
+
85
+ function completeItemArg(prefix: string): AutocompleteItem[] | null {
86
+ refreshIfStale();
87
+ const normalized = prefix.toLowerCase();
88
+ const matches = itemCache.items
89
+ .filter((item) => item.id.toLowerCase().startsWith(normalized) || item.title.toLowerCase().includes(normalized))
90
+ .slice(0, 20)
91
+ .map((item) => ({ value: item.id, label: `${item.id.slice(0, 8)}… ${item.title}`.trim() }));
92
+ return matches.length > 0 ? matches : null;
93
+ }
94
+
95
+ async function resolveItemArg(arg: string): Promise<{ id: string } | { error: string }> {
96
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(arg)) {
97
+ return { id: arg };
98
+ }
99
+ if (!await refreshItemCache()) {
100
+ return { error: "Could not refresh DevSpec tasks. No work was started." };
101
+ }
102
+ const normalized = arg.toLowerCase();
103
+ const matches = itemCache.items.filter(
104
+ (item) => item.id.toLowerCase().startsWith(normalized) || item.title.toLowerCase().includes(normalized),
105
+ );
106
+ if (matches.length === 1) return { id: matches[0]!.id };
107
+ if (matches.length > 1) {
108
+ const listing = matches.slice(0, 5).map((item) => ` ${item.id.slice(0, 8)}… ${item.title}`).join("\n");
109
+ return { error: `Ambiguous item “${arg}” — matches:\n${listing}\nPass a fuller id.` };
110
+ }
111
+ return { error: `No open task matches “${arg}”. Pass an id/title or run /devspec.work to choose explicitly.` };
112
+ }
113
+
114
+ async function selectExplicitItem(ctx: ExtensionContext): Promise<string | null> {
115
+ if (!await refreshItemCache(ctx.cwd)) {
116
+ ctx.ui.notify("Could not refresh DevSpec tasks — no work was started.", "warning");
117
+ return null;
118
+ }
119
+ if (itemCache.items.length === 0) {
120
+ ctx.ui.notify("No open DevSpec tasks are available to select.", "info");
121
+ return null;
122
+ }
123
+ if (!ctx.hasUI) {
124
+ ctx.ui.notify("Pass an explicit task id to /devspec.work in this mode.", "warning");
125
+ return null;
126
+ }
127
+ const labels = itemCache.items.map((item) => `${item.id.slice(0, 8)}… ${item.title}`.trim());
128
+ const chosen = await ctx.ui.select("Choose one task to work on:", labels);
129
+ if (!chosen) return null;
130
+ const index = labels.indexOf(chosen);
131
+ return index >= 0 ? itemCache.items[index]!.id : null;
132
+ }
133
+
134
+ async function fetchItemById(id: string): Promise<WorkTarget> {
135
+ const resolved = await resolveToken();
136
+ if (!resolved) throw new Error("not connected");
137
+ const raw = await callDevspec(
138
+ resolved.token,
139
+ "get_task",
140
+ { task_id: id },
141
+ { lane: "work", timeoutMs: MODEL_TIMEOUT_MS },
142
+ );
143
+ const { details, text, isError } = unwrapMcpResult(raw);
144
+ if (isError) throw new Error(text || "get_task failed");
145
+ const root = details as { id?: string; title?: string; item?: { id?: string; title?: string } } | null;
146
+ const item = (root?.item ?? root) as { id?: string; title?: string } | null;
147
+ return { id: item?.id ?? id, title: item?.title, payload: details ?? text };
148
+ }
149
+
150
+ export default function (pi: ExtensionAPI) {
151
+ pi.on("session_start", (_event, ctx) => {
152
+ cacheCwd = ctx.cwd;
153
+ itemCache = { at: 0, scope: "", items: [] };
154
+ void refreshItemCache(ctx.cwd);
155
+ });
156
+
157
+ pi.registerCommand("devspec.work", {
158
+ description: "Select one explicit DevSpec task to work on.",
159
+ getArgumentCompletions: completeItemArg,
160
+ handler: async (args, ctx: ExtensionContext) => {
161
+ const resolved = await resolveToken();
162
+ if (!resolved) {
163
+ ctx.ui.notify("Not connected to DevSpec — run /devspec.login first.", "warning");
164
+ return;
165
+ }
166
+ if (!ctx.isIdle()) {
167
+ ctx.ui.notify("Agent is busy — use /devspec.work when the current turn finishes.", "warning");
168
+ return;
169
+ }
170
+
171
+ const arg = args.trim();
172
+ let id: string | null = null;
173
+ if (arg) {
174
+ const match = await resolveItemArg(arg);
175
+ if ("error" in match) {
176
+ ctx.ui.notify(match.error, "warning");
177
+ return;
178
+ }
179
+ id = match.id;
180
+ } else {
181
+ id = await selectExplicitItem(ctx);
182
+ }
183
+ if (!id) return;
184
+
185
+ try {
186
+ const [target, implementationContext] = await Promise.all([
187
+ fetchItemById(id),
188
+ loadImplementationContext({
189
+ loader: createImplementationContractLoader(resolved.token),
190
+ }),
191
+ ]);
192
+ const kickoff = buildImplementationKickoff({
193
+ taskIds: [target.id],
194
+ context: implementationContext,
195
+ });
196
+ const delivery = recordActiveDelivery(kickoff, implementationContext);
197
+ const prompt = buildWorkPrompt(target, implementationContext);
198
+ // The owner typed /devspec.work here: the prompt it sends is their
199
+ // request, admitted like anything they type (item 7a4d04fd).
200
+ markTypedSend(prompt);
201
+ try {
202
+ pi.sendUserMessage(prompt);
203
+ } catch (error) {
204
+ unmarkTypedSend(prompt);
205
+ rollbackActiveDelivery(delivery);
206
+ throw error;
207
+ }
208
+ if (implementationContext.warning) ctx.ui.notify(implementationContext.warning, "warning");
209
+ void refreshItemCache(ctx.cwd);
210
+ noteWork({ id: target.id, title: target.title });
211
+ paintStatus(ctx);
212
+ } catch (error) {
213
+ ctx.ui.notify(`DevSpec: ${error instanceof Error ? error.message : String(error)}`, "error");
214
+ }
215
+ },
216
+ });
217
+
218
+ pi.registerCommand("devspec.help", {
219
+ description: "What the DevSpec package provides, and connection state.",
220
+ handler: async (_args, ctx) => {
221
+ const resolved = await resolveToken();
222
+ const lines = [
223
+ "DevSpec commands:",
224
+ " /devspec.work [item] — select one task to work (id/title, or picker)",
225
+ " /devspec.remote — connect (optional --project <name-or-id>; stop | auto on|off)",
226
+ " /devspec.project — show project; choose | remember | forget",
227
+ " pi --devspec-project <name-or-id> — choose for a new local conversation",
228
+ " /devspec.reload — reload this extension (same as /reload)",
229
+ ` /devspec.login — connect (token from ${agentsSettingsUrl()})`,
230
+ " /devspec.pin <id> — explicitly associate this folder with a DevSpec project",
231
+ " /devspec.status — connection state",
232
+ " /devspec.logout — remove the stored token",
233
+ " /devspec.logo — toggle the duck header",
234
+ "",
235
+ "Your owner can ask you to take work through DevSpec remote control; you reserve the selected items yourself. Nothing is pushed to you, and this command never drains a queue.",
236
+ "DevSpec tools are native to this package. Foreign MCP servers remain available.",
237
+ `Connection: ${resolved ? `token from ${resolved.source}` : "not connected — /devspec.login"}`,
238
+ ];
239
+ const guard = getAdapterGuardState();
240
+ if (guard.suppressed.length > 0) lines.push("", SUPPRESSION_NOTICE);
241
+ ctx.ui.notify(lines.join("\n"), "info");
242
+ },
243
+ });
244
+ }
@@ -0,0 +1,18 @@
1
+ This file is the **wire map**, not the model surface.
2
+
3
+ Generated from the DevSpec MCP tool definitions. The embedded
4
+ client speaks these verb names on `/api/mcp`. What the model sees is the
5
+ Pi-shaped hot set in `extensions/core/model-surface.ts`.
6
+ Do not regenerate this catalog and activate the whole verb set. Every safe model
7
+ verb is registered natively, but only the small hot set (plus narrowly activated
8
+ `manage_plan` / `manage_poll` for bonded session work) is active without deferred search.
9
+
10
+ Do not edit `mcp-catalog.ts` by hand. Regenerate:
11
+
12
+ ```
13
+ npm run generate:catalog
14
+ # or, from an exported tool schema file:
15
+ node scripts/generate-mcp-catalog.mjs --from /path/to/devspec-mcp-tools.json
16
+ ```
17
+
18
+ The drift test (`tests/catalog-drift.test.ts`) verifies that the generated verb list stays in sync with the server schema.