@zihanw/pi-forge 0.5.4 → 0.5.5

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 (228) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +67 -103
  3. package/README.zh-CN.md +67 -95
  4. package/assets/pi-forge-header-concept-1.png +0 -0
  5. package/assets/readme/PROVENANCE.md +95 -0
  6. package/assets/readme/en/capability-tools.gif +0 -0
  7. package/assets/readme/en/context-composition.gif +0 -0
  8. package/assets/readme/en/context-toggle.gif +0 -0
  9. package/assets/readme/en/draft-diff.png +0 -0
  10. package/assets/readme/en/edit-draft-diff.gif +0 -0
  11. package/assets/readme/en/editor-overview-v3.png +0 -0
  12. package/assets/readme/en/editor-overview.png +0 -0
  13. package/assets/readme/en/mode-tools.gif +0 -0
  14. package/assets/readme/en/regex-transforms.gif +0 -0
  15. package/assets/readme/en/tool-selection.gif +0 -0
  16. package/assets/readme/tui-quickstart.gif +0 -0
  17. package/assets/readme/zh-CN/capability-tools.gif +0 -0
  18. package/assets/readme/zh-CN/context-composition.gif +0 -0
  19. package/assets/readme/zh-CN/context-toggle.gif +0 -0
  20. package/assets/readme/zh-CN/draft-diff.png +0 -0
  21. package/assets/readme/zh-CN/edit-draft-diff.gif +0 -0
  22. package/assets/readme/zh-CN/editor-overview-v3.png +0 -0
  23. package/assets/readme/zh-CN/editor-overview.png +0 -0
  24. package/assets/readme/zh-CN/mode-tools.gif +0 -0
  25. package/assets/readme/zh-CN/regex-transforms.gif +0 -0
  26. package/assets/readme/zh-CN/tool-selection.gif +0 -0
  27. package/dist/active-state.d.ts +148 -0
  28. package/dist/active-state.d.ts.map +1 -0
  29. package/dist/active-state.js +374 -0
  30. package/dist/active-state.js.map +1 -0
  31. package/dist/agent-profile.d.ts.map +1 -1
  32. package/dist/agent-profile.js +11 -0
  33. package/dist/agent-profile.js.map +1 -1
  34. package/dist/capabilities.d.ts +39 -0
  35. package/dist/capabilities.d.ts.map +1 -0
  36. package/dist/capabilities.js +160 -0
  37. package/dist/capabilities.js.map +1 -0
  38. package/dist/capability-anchors.d.ts +45 -0
  39. package/dist/capability-anchors.d.ts.map +1 -0
  40. package/dist/capability-anchors.js +263 -0
  41. package/dist/capability-anchors.js.map +1 -0
  42. package/dist/capability-command.d.ts +4 -0
  43. package/dist/capability-command.d.ts.map +1 -0
  44. package/dist/capability-command.js +164 -0
  45. package/dist/capability-command.js.map +1 -0
  46. package/dist/capability-events.d.ts +79 -0
  47. package/dist/capability-events.d.ts.map +1 -0
  48. package/dist/capability-events.js +478 -0
  49. package/dist/capability-events.js.map +1 -0
  50. package/dist/capability-projection.d.ts +22 -0
  51. package/dist/capability-projection.d.ts.map +1 -0
  52. package/dist/capability-projection.js +273 -0
  53. package/dist/capability-projection.js.map +1 -0
  54. package/dist/capability-protocol.d.ts +21 -0
  55. package/dist/capability-protocol.d.ts.map +1 -0
  56. package/dist/capability-protocol.js +15 -0
  57. package/dist/capability-protocol.js.map +1 -0
  58. package/dist/capability-state.d.ts +74 -0
  59. package/dist/capability-state.d.ts.map +1 -0
  60. package/dist/capability-state.js +43 -0
  61. package/dist/capability-state.js.map +1 -0
  62. package/dist/capability-tool.d.ts +11 -0
  63. package/dist/capability-tool.d.ts.map +1 -0
  64. package/dist/capability-tool.js +25 -0
  65. package/dist/capability-tool.js.map +1 -0
  66. package/dist/capability-web-host.d.ts +11 -0
  67. package/dist/capability-web-host.d.ts.map +1 -0
  68. package/dist/capability-web-host.js +105 -0
  69. package/dist/capability-web-host.js.map +1 -0
  70. package/dist/codecs/capability.d.ts +71 -0
  71. package/dist/codecs/capability.d.ts.map +1 -0
  72. package/dist/codecs/capability.js +363 -0
  73. package/dist/codecs/capability.js.map +1 -0
  74. package/dist/codecs/prompt-stack.d.ts +1 -1
  75. package/dist/codecs/prompt-stack.d.ts.map +1 -1
  76. package/dist/codecs/prompt-stack.js +140 -13
  77. package/dist/codecs/prompt-stack.js.map +1 -1
  78. package/dist/command-contribution/index.d.ts +21 -0
  79. package/dist/command-contribution/index.d.ts.map +1 -0
  80. package/dist/command-contribution/index.js +14 -0
  81. package/dist/command-contribution/index.js.map +1 -0
  82. package/dist/compile-cycle.d.ts +1 -0
  83. package/dist/compile-cycle.d.ts.map +1 -1
  84. package/dist/compile-cycle.js.map +1 -1
  85. package/dist/compiler.d.ts.map +1 -1
  86. package/dist/compiler.js +70 -17
  87. package/dist/compiler.js.map +1 -1
  88. package/dist/forge-command.d.ts +10 -0
  89. package/dist/forge-command.d.ts.map +1 -0
  90. package/dist/forge-command.js +106 -0
  91. package/dist/forge-command.js.map +1 -0
  92. package/dist/index.d.ts +2 -2
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +54 -5
  95. package/dist/index.js.map +1 -1
  96. package/dist/json-fingerprint.d.ts +11 -0
  97. package/dist/json-fingerprint.d.ts.map +1 -0
  98. package/dist/json-fingerprint.js +66 -0
  99. package/dist/json-fingerprint.js.map +1 -0
  100. package/dist/lifecycle.d.ts +14 -0
  101. package/dist/lifecycle.d.ts.map +1 -1
  102. package/dist/lifecycle.js +167 -67
  103. package/dist/lifecycle.js.map +1 -1
  104. package/dist/payload-command.d.ts +2 -2
  105. package/dist/payload-command.d.ts.map +1 -1
  106. package/dist/payload-command.js +80 -26
  107. package/dist/payload-command.js.map +1 -1
  108. package/dist/payload-state.d.ts +1 -0
  109. package/dist/payload-state.d.ts.map +1 -1
  110. package/dist/payload-state.js +1 -0
  111. package/dist/payload-state.js.map +1 -1
  112. package/dist/policy.d.ts +2 -1
  113. package/dist/policy.d.ts.map +1 -1
  114. package/dist/policy.js +3 -0
  115. package/dist/policy.js.map +1 -1
  116. package/dist/preset-command.d.ts.map +1 -1
  117. package/dist/preset-command.js +92 -35
  118. package/dist/preset-command.js.map +1 -1
  119. package/dist/preview-text.d.ts +5 -0
  120. package/dist/preview-text.d.ts.map +1 -0
  121. package/dist/preview-text.js +27 -0
  122. package/dist/preview-text.js.map +1 -0
  123. package/dist/preview.d.ts +18 -1
  124. package/dist/preview.d.ts.map +1 -1
  125. package/dist/preview.js +132 -97
  126. package/dist/preview.js.map +1 -1
  127. package/dist/profile-command.js +68 -9
  128. package/dist/profile-command.js.map +1 -1
  129. package/dist/regex.d.ts.map +1 -1
  130. package/dist/regex.js +5 -0
  131. package/dist/regex.js.map +1 -1
  132. package/dist/render-helpers.d.ts.map +1 -1
  133. package/dist/render-helpers.js +2 -0
  134. package/dist/render-helpers.js.map +1 -1
  135. package/dist/repositories/capability.d.ts +53 -0
  136. package/dist/repositories/capability.d.ts.map +1 -0
  137. package/dist/repositories/capability.js +294 -0
  138. package/dist/repositories/capability.js.map +1 -0
  139. package/dist/runtime/capability-runtime.d.ts +91 -0
  140. package/dist/runtime/capability-runtime.d.ts.map +1 -0
  141. package/dist/runtime/capability-runtime.js +967 -0
  142. package/dist/runtime/capability-runtime.js.map +1 -0
  143. package/dist/runtime/tool-policy-runtime.d.ts +8 -0
  144. package/dist/runtime/tool-policy-runtime.d.ts.map +1 -1
  145. package/dist/runtime/tool-policy-runtime.js +171 -30
  146. package/dist/runtime/tool-policy-runtime.js.map +1 -1
  147. package/dist/session-adapter.d.ts +13 -0
  148. package/dist/session-adapter.d.ts.map +1 -1
  149. package/dist/session-adapter.js +110 -0
  150. package/dist/session-adapter.js.map +1 -1
  151. package/dist/session-usage.d.ts +65 -0
  152. package/dist/session-usage.d.ts.map +1 -0
  153. package/dist/session-usage.js +134 -0
  154. package/dist/session-usage.js.map +1 -0
  155. package/dist/subagent/fingerprints.d.ts +3 -15
  156. package/dist/subagent/fingerprints.d.ts.map +1 -1
  157. package/dist/subagent/fingerprints.js +5 -69
  158. package/dist/subagent/fingerprints.js.map +1 -1
  159. package/dist/subagent/index.d.ts +2 -0
  160. package/dist/subagent/index.d.ts.map +1 -1
  161. package/dist/subagent/index.js +2 -0
  162. package/dist/subagent/index.js.map +1 -1
  163. package/dist/subagent-host.d.ts +2 -2
  164. package/dist/subagent-host.d.ts.map +1 -1
  165. package/dist/subagent-host.js +13 -1
  166. package/dist/subagent-host.js.map +1 -1
  167. package/dist/types.d.ts +6 -1
  168. package/dist/types.d.ts.map +1 -1
  169. package/dist/types.js.map +1 -1
  170. package/dist/web-editor/client-script.generated.d.ts.map +1 -1
  171. package/dist/web-editor/client-script.generated.js +1 -1
  172. package/dist/web-editor/client-script.generated.js.map +1 -1
  173. package/dist/web-editor/client-styles.generated.d.ts.map +1 -1
  174. package/dist/web-editor/client-styles.generated.js +1 -1
  175. package/dist/web-editor/client-styles.generated.js.map +1 -1
  176. package/dist/web-editor/server.d.ts.map +1 -1
  177. package/dist/web-editor/server.js +152 -4
  178. package/dist/web-editor/server.js.map +1 -1
  179. package/dist/web-editor/styles.d.ts.map +1 -1
  180. package/dist/web-editor/styles.js +476 -149
  181. package/dist/web-editor/styles.js.map +1 -1
  182. package/dist/web-editor/types.d.ts +81 -1
  183. package/dist/web-editor/types.d.ts.map +1 -1
  184. package/dist/web-host.d.ts +7 -0
  185. package/dist/web-host.d.ts.map +1 -1
  186. package/dist/web-host.js +168 -5
  187. package/dist/web-host.js.map +1 -1
  188. package/dist/workspace.d.ts +11 -0
  189. package/dist/workspace.d.ts.map +1 -1
  190. package/dist/workspace.js +63 -5
  191. package/dist/workspace.js.map +1 -1
  192. package/docs/README.md +4 -0
  193. package/docs/design/README.md +3 -1
  194. package/docs/design/architecture-0.5.md +22 -0
  195. package/docs/design/archive/2026-09-12-system-update-design.md +229 -0
  196. package/docs/design/pi-forge-system-update-design-notes.md +157 -229
  197. package/docs/development/release.md +20 -20
  198. package/docs/development/roadmap.md +17 -2
  199. package/docs/development/scoped-global-profiles-stacks.md +1 -1
  200. package/docs/development/setup.md +1 -1
  201. package/docs/getting-started.md +1 -1
  202. package/docs/guides/delegation.md +22 -20
  203. package/docs/guides/migrating-to-0.5.md +29 -0
  204. package/docs/guides/use-cases.md +6 -2
  205. package/docs/guides/web-editor.md +73 -5
  206. package/docs/reference/active-state.md +77 -0
  207. package/docs/reference/capabilities.md +259 -0
  208. package/docs/reference/commands.md +52 -24
  209. package/docs/reference/configuration.md +4 -2
  210. package/docs/reference/features.md +70 -4
  211. package/docs/reference/provider-support.md +67 -0
  212. package/docs/reference/public-api.md +49 -3
  213. package/docs/reference/session-cache.md +112 -0
  214. package/docs/reference/stack-schema.md +18 -4
  215. package/docs/reference/subagent-host-port.md +8 -0
  216. package/docs/zh-CN/README.md +3 -0
  217. package/docs/zh-CN/getting-started.md +1 -1
  218. package/docs/zh-CN/guides/delegation.md +22 -10
  219. package/docs/zh-CN/guides/migrating-to-0.5.md +29 -0
  220. package/docs/zh-CN/guides/web-editor.md +74 -7
  221. package/docs/zh-CN/reference/capabilities.md +259 -0
  222. package/docs/zh-CN/reference/commands.md +61 -33
  223. package/docs/zh-CN/reference/provider-support.md +67 -0
  224. package/docs/zh-CN/reference/session-cache.md +112 -0
  225. package/examples/capabilities/review.json +12 -0
  226. package/examples/capabilities/write-tools.json +15 -0
  227. package/examples/read-first-worker-prompt-stack.json +49 -0
  228. package/package.json +16 -9
@@ -0,0 +1,967 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { summarizeSessionCacheUsage } from "../session-usage.js";
3
+ import { fingerprintJson } from "../json-fingerprint.js";
4
+ import { isCapabilityStateMutation, isCapabilityEnableRequest, } from "../capability-state.js";
5
+ import { createResourceCatalog } from "../catalog.js";
6
+ import { createCapabilitySnapshot, reduceCapabilityEvents, MAX_ID_LENGTH } from "../capability-events.js";
7
+ import { resolveCapability, resolveCapabilityBindings } from "../capabilities.js";
8
+ import { isUsableCapability } from "../codecs/capability.js";
9
+ import { projectCapabilityMessages } from "../capability-projection.js";
10
+ import { isCapabilityDelivery } from "../capability-protocol.js";
11
+ import { formatResourceKey, parseResourceSelector } from "../resource-identity.js";
12
+ import { hasToolSelectionPolicy } from "../policy.js";
13
+ import { buildSessionProjection, } from "@earendil-works/pi-coding-agent";
14
+ import { hasPendingCapabilityToolCalls, capabilityContextMatches, materializeCapabilityAnchors, } from "../capability-anchors.js";
15
+ import { getCurrentBranchEntries, persistCapabilityDelivery, persistCapabilityEvent, persistCapabilityTools, readCapabilitySession, } from "../session-adapter.js";
16
+ function isPlainObject(value) {
17
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
18
+ return false;
19
+ }
20
+ const proto = Object.getPrototypeOf(value);
21
+ return proto === Object.prototype || proto === null;
22
+ }
23
+ /** Branch entries are authoritative. This service only coordinates the existing tool owner and delivery. */
24
+ export function createCapabilityRuntime(pi, workspace, tools) {
25
+ const instanceId = randomUUID();
26
+ let context;
27
+ let restoring = false;
28
+ let preparedRevision;
29
+ let preparedModel;
30
+ let lastToolRecord;
31
+ let restoredTools;
32
+ let agentBusy = false;
33
+ let disposed = false;
34
+ let lifecycleRevision = 0;
35
+ function setAgentBusy(busy) {
36
+ agentBusy = busy;
37
+ }
38
+ function isBusy(ctx) {
39
+ if (agentBusy)
40
+ return true;
41
+ const targetCtx = ctx ?? context;
42
+ if (targetCtx && typeof targetCtx.isIdle === "function") {
43
+ return !targetCtx.isIdle();
44
+ }
45
+ return false;
46
+ }
47
+ function view(ctx) {
48
+ const history = readCapabilitySession(ctx);
49
+ const state = reduceCapabilityEvents(history.events);
50
+ if (!state.ok)
51
+ throw new Error(state.error);
52
+ return { history, state };
53
+ }
54
+ function rememberTools(ctx) {
55
+ const history = readCapabilitySession(ctx);
56
+ if (!history.events.length && !history.tools && !hasToolSelectionPolicy(workspace.snapshotKnown ? workspace.snapshot().active?.stack.tools : undefined))
57
+ return;
58
+ const snapshot = tools.snapshot();
59
+ const serialized = JSON.stringify(snapshot);
60
+ if (serialized !== lastToolRecord) {
61
+ persistCapabilityTools(pi, snapshot);
62
+ lastToolRecord = serialized;
63
+ }
64
+ }
65
+ function prepareRestore(ctx) {
66
+ disposed = false;
67
+ lifecycleRevision++;
68
+ context = ctx;
69
+ restoring = true;
70
+ preparedRevision = undefined;
71
+ preparedModel = undefined;
72
+ lastToolRecord = undefined;
73
+ agentBusy = false;
74
+ const history = readCapabilitySession(ctx);
75
+ // A branch without a durable baseline must not replace an existing pristine
76
+ // baseline with the currently filtered selection. Let its owner reconcile it.
77
+ restoredTools = history.tools;
78
+ tools.setCapabilities([]);
79
+ }
80
+ function restore(ctx, options) {
81
+ if (disposed)
82
+ return;
83
+ context = ctx;
84
+ restoring = false;
85
+ agentBusy = false;
86
+ if (!options?.deferToolPolicy)
87
+ sync(ctx);
88
+ }
89
+ function sync(ctx = context) {
90
+ if (restoring)
91
+ return;
92
+ if (!ctx) {
93
+ tools.sync(ctx);
94
+ return;
95
+ }
96
+ context = ctx;
97
+ let current = view(ctx);
98
+ const preset = workspace.snapshotKnown && workspace.snapshot().active;
99
+ const presetKey = preset ? formatResourceKey(preset.key) : undefined;
100
+ let lastRetired;
101
+ for (const item of current.state.active) {
102
+ const source = item.snapshot.source;
103
+ if (source.kind !== "capability" || !source.binding || formatResourceKey(source.binding.preset) === presetKey)
104
+ continue;
105
+ const event = {
106
+ schemaVersion: 1, eventId: randomUUID(), op: "deactivate", actor: "lifecycle",
107
+ createdAt: Date.now(), activationId: item.snapshot.activationId,
108
+ };
109
+ persistCapabilityEvent(pi, event);
110
+ lastRetired = event.eventId;
111
+ }
112
+ if (lastRetired) {
113
+ if (!isBusy(ctx))
114
+ persistPendingAnchors(ctx);
115
+ current = view(ctx);
116
+ }
117
+ if (!ctx.isProjectTrusted() && current.state.active.length) {
118
+ throw new Error("Active capabilities require a trusted project. Use /capability reset to clear them, or trust the project.");
119
+ }
120
+ const patches = current.state.active.map((item) => item.snapshot.tools);
121
+ const error = tools.validateCapabilities(patches);
122
+ if (error)
123
+ throw new Error(error);
124
+ tools.setCapabilities(patches, restoredTools);
125
+ restoredTools = undefined;
126
+ tools.sync(ctx);
127
+ rememberTools(ctx);
128
+ }
129
+ function pendingEvents(ctx) {
130
+ const history = readCapabilitySession(ctx);
131
+ const checkpoint = history.events.findIndex(event => event.eventId === history.checkpointThrough);
132
+ const through = Math.max(history.lastAnchoredIndex, checkpoint);
133
+ const seen = new Set();
134
+ return history.events.filter((event, index) => {
135
+ if (seen.has(event.eventId))
136
+ return false;
137
+ seen.add(event.eventId);
138
+ return index > through;
139
+ });
140
+ }
141
+ function persistPendingAnchors(ctx) {
142
+ // Do not collapse a saved-but-unanchored activate/off sequence into net state.
143
+ for (const event of pendingEvents(ctx))
144
+ persistCapabilityDelivery(pi, event.eventId);
145
+ }
146
+ function canonicalSessionMessages(ctx) {
147
+ return buildSessionProjection(getCurrentBranchEntries(ctx)).messages;
148
+ }
149
+ function prepareMessages(raw, ctx) {
150
+ context = ctx;
151
+ if (pendingEvents(ctx).length > 0) {
152
+ // Context hooks should run after a complete batch. Never project pending
153
+ // deltas into an unresolved current batch if that invariant is broken.
154
+ if (hasPendingCapabilityToolCalls(raw)) {
155
+ throw new Error("Cannot prepare capability anchors during an incomplete tool batch.");
156
+ }
157
+ if (!capabilityContextMatches(canonicalSessionMessages(ctx), raw)) {
158
+ throw new Error("Cannot materialize capability anchors: context has no unique session alignment (possible preceding extension rewrite or deferred custom messages)");
159
+ }
160
+ persistPendingAnchors(ctx);
161
+ }
162
+ return materializeCapabilityAnchors(getCurrentBranchEntries(ctx), raw);
163
+ }
164
+ function project(messages, ctx) {
165
+ const { history, state } = view(ctx);
166
+ if (!ctx.isProjectTrusted())
167
+ return messages.filter((message) => !isCapabilityDelivery(message));
168
+ const native = ctx.model?.compat?.supportsMidConvoSystemMessages === true;
169
+ const projected = projectCapabilityMessages(messages, history, native);
170
+ preparedRevision = projected.throughEventId;
171
+ preparedModel = modelKey(ctx);
172
+ if (state.lastEventId)
173
+ ctx.ui.setStatus("pi-forge-capabilities", `${state.active.length} capability(s) · request prepared`);
174
+ return projected.messages;
175
+ }
176
+ function commit(ctx, event) {
177
+ const current = view(ctx);
178
+ const next = reduceCapabilityEvents([...current.history.events, event]);
179
+ if (!next.ok)
180
+ throw new Error(next.error);
181
+ const problem = tools.validateCapabilities(next.active.map((item) => item.snapshot.tools));
182
+ if (problem)
183
+ throw new Error(problem);
184
+ // Persist the pristine/reconciled baseline before a branch can land on the activation entry.
185
+ tools.sync(ctx);
186
+ const before = tools.snapshot();
187
+ if (JSON.stringify(current.history.tools) !== JSON.stringify(before)) {
188
+ persistCapabilityTools(pi, before);
189
+ lastToolRecord = JSON.stringify(before);
190
+ }
191
+ persistCapabilityEvent(pi, event);
192
+ // Any failure after saving intent remains recoverable; never imply rollback.
193
+ try {
194
+ if (!isBusy(ctx))
195
+ persistPendingAnchors(ctx);
196
+ sync(ctx);
197
+ ctx.ui.setStatus("pi-forge-capabilities", `${next.active.length} capability(s) · pending request`);
198
+ }
199
+ catch (error) {
200
+ preparedRevision = undefined;
201
+ throw new Error(`Capability event saved but application is pending: ${error instanceof Error ? error.message : "delivery failure"}`);
202
+ }
203
+ }
204
+ function library(ctx) {
205
+ const capabilities = workspace.reloadCapabilities(ctx.cwd, ctx.isProjectTrusted()).capabilities;
206
+ return capabilities.length ? capabilities.map((loaded) => {
207
+ const errors = loaded.diagnostics.filter((d) => d.level === "error").map((d) => d.message);
208
+ return `${formatResourceKey(loaded.key)}${loaded.capability.name ? ` — ${loaded.capability.name}` : ""}${errors.length ? ` [invalid: ${errors.join("; ")}]` : ""}`;
209
+ }).join("\n") : "No capabilities. Place JSON in .pi/forge/capabilities/ or ~/.pi/forge/capabilities/.";
210
+ }
211
+ /**
212
+ * Completion-only projection. It deliberately consumes the current session
213
+ * and the last published workspace snapshot; unlike library()/readBindings()
214
+ * it never reloads files or publishes a snapshot on a keystroke.
215
+ */
216
+ function completionView(ctx) {
217
+ const target = ctx ?? context;
218
+ if (disposed || restoring || !target || (ctx && !sameContext(context, ctx))) {
219
+ return { ok: false, error: "Capability runtime is not active for this session." };
220
+ }
221
+ try {
222
+ const trusted = target.isProjectTrusted();
223
+ const snapshot = workspace.snapshot();
224
+ if (!workspace.snapshotKnown || snapshot.cwd !== target.cwd)
225
+ return { ok: false, error: "Workspace completion snapshot does not match the current project." };
226
+ const { state } = view(target);
227
+ const active = state.active.map((item) => {
228
+ const source = sourceLabel(item);
229
+ const name = item.snapshot.name;
230
+ return {
231
+ id: item.snapshot.activationId,
232
+ label: `${source}${name ? ` — ${name}` : ""} [${item.actor}]`,
233
+ };
234
+ });
235
+ if (!trusted) {
236
+ return { ok: true, trusted: false, capturedAt: snapshot.capturedAt, capabilities: [], bindings: [], active };
237
+ }
238
+ // Invalid project definitions still shadow global ones; count before filtering.
239
+ const collidingIds = new Set();
240
+ const byId = new Map();
241
+ for (const loaded of snapshot.capabilities) {
242
+ const count = (byId.get(loaded.key.id) ?? 0) + 1;
243
+ byId.set(loaded.key.id, count);
244
+ if (count === 2)
245
+ collidingIds.add(loaded.key.id);
246
+ }
247
+ const capabilities = snapshot.capabilities
248
+ .filter((loaded) => isUsableCapability(loaded))
249
+ .map((loaded) => ({
250
+ id: formatResourceKey(loaded.key),
251
+ bareId: loaded.key.id,
252
+ label: `${formatResourceKey(loaded.key)}${loaded.capability.name ? ` — ${loaded.capability.name}` : ""}`,
253
+ collides: collidingIds.has(loaded.key.id),
254
+ }));
255
+ const bindings = [];
256
+ const preset = snapshot.active;
257
+ if (preset && (preset.stack.capabilities?.length ?? 0) > 0) {
258
+ const resolved = resolveCapabilityBindings(createResourceCatalog([...snapshot.capabilities]), preset.key, preset.stack.capabilities ?? []);
259
+ if (!resolved.ok)
260
+ return { ok: false, error: resolved.error };
261
+ for (const binding of resolved.bindings) {
262
+ const readable = binding.capability.name ?? formatResourceKey(binding.ref);
263
+ bindings.push({
264
+ id: binding.id,
265
+ modelCallable: binding.modelCallable,
266
+ label: `${binding.id} — ${readable}${binding.modelCallable ? "" : " [human-only]"}`,
267
+ });
268
+ }
269
+ }
270
+ return { ok: true, trusted: true, capturedAt: snapshot.capturedAt, capabilities, bindings, active };
271
+ }
272
+ catch (error) {
273
+ return { ok: false, error: `Capability completion unavailable: ${error instanceof Error ? error.message : String(error)}` };
274
+ }
275
+ }
276
+ function modelKey(ctx) {
277
+ return JSON.stringify([ctx.model?.provider, ctx.model?.id, ctx.model?.compat?.supportsMidConvoSystemMessages === true]);
278
+ }
279
+ /** Reading the browser view must never append entries, sync tools or start inference. */
280
+ function readState() {
281
+ const ctx = context;
282
+ if (!ctx)
283
+ return { ok: false, status: 503, error: "No active Forge session." };
284
+ try {
285
+ const { history, state } = view(ctx);
286
+ const active = state.active.map((item) => ({
287
+ activationId: item.snapshot.activationId,
288
+ source: sourceLabel(item),
289
+ ...(item.snapshot.name === undefined ? {} : { name: item.snapshot.name }),
290
+ actor: item.actor,
291
+ content: item.snapshot.content,
292
+ tools: { add: [...item.snapshot.tools.add], remove: [...item.snapshot.tools.remove] },
293
+ }));
294
+ const trusted = ctx.isProjectTrusted();
295
+ const effectiveTools = [...pi.getActiveTools()];
296
+ const model = modelKey(ctx);
297
+ const delivery = !state.lastEventId ? "none" : preparedRevision === state.lastEventId && preparedModel === model ? "prepared" : "pending";
298
+ const problem = tools.validateCapabilities(active.map((item) => item.tools));
299
+ const sessionId = ctx.sessionManager.getSessionId();
300
+ const leafId = ctx.sessionManager.getLeafId();
301
+ const preset = workspace.snapshotKnown ? workspace.snapshot().active : undefined;
302
+ const revision = fingerprintJson({ domain: "forge-capability-view-v1", instanceId, lifecycleRevision, sessionId, leafId,
303
+ lastEventId: state.lastEventId, active, effectiveTools, trusted, restoring, model, delivery,
304
+ preset: preset ? { key: preset.key, tools: preset.stack.tools, capabilities: preset.stack.capabilities ?? [] } : null, baseline: history.tools });
305
+ const result = {
306
+ guard: { sessionId, leafId, revision }, trusted, restoring, delivery,
307
+ ...(preset ? { presetRevision: fingerprintJson({ domain: "forge-inspection-preset-v1", key: preset.key, stack: preset.stack }) } : {}),
308
+ textPresentation: ctx.model?.compat?.supportsMidConvoSystemMessages === true ? "native" : "user",
309
+ effectiveTools, active, ...(problem ? { problem } : {}),
310
+ // Derived from persisted usage only; leafId already changes the guard for each new request.
311
+ cacheUsage: summarizeSessionCacheUsage(getCurrentBranchEntries(ctx)),
312
+ };
313
+ return { ok: true, state: result };
314
+ }
315
+ catch (error) {
316
+ return { ok: false, status: 503, error: `Capability state unavailable: ${error instanceof Error ? error.message : String(error)}` };
317
+ }
318
+ }
319
+ function choiceForCapability(kind, id, capability, source) {
320
+ const snapshot = createCapabilitySnapshot({
321
+ activationId: randomUUID(),
322
+ source,
323
+ ...(capability.name === undefined ? {} : { name: capability.name }),
324
+ content: capability.content,
325
+ tools: capability.tools,
326
+ });
327
+ const problem = tools.validateCapabilities([capability.tools]);
328
+ return {
329
+ kind,
330
+ id,
331
+ label: capability.name ?? id,
332
+ content: capability.content,
333
+ tools: { add: [...capability.tools.add], remove: [...capability.tools.remove] },
334
+ fingerprint: snapshot.fingerprint,
335
+ ...(problem ? { problem } : {}),
336
+ };
337
+ }
338
+ /** Read-only resource discovery for the human web client. */
339
+ function readAvailableCapabilities() {
340
+ const stateResult = readState();
341
+ if (!stateResult.ok)
342
+ return stateResult;
343
+ const ctx = context;
344
+ if (!ctx)
345
+ return { ok: false, status: 503, error: "No active Forge session." };
346
+ if (!stateResult.state.trusted)
347
+ return { ok: false, status: 403, error: "Project is not trusted." };
348
+ if (stateResult.state.restoring || disposed)
349
+ return { ok: false, status: 503, error: "Capability session is restoring." };
350
+ try {
351
+ if (!ctx.isProjectTrusted())
352
+ return { ok: false, status: 403, error: "Project is not trusted." };
353
+ const capabilities = workspace.reloadCapabilities(ctx.cwd, ctx.isProjectTrusted()).capabilities;
354
+ const choices = [];
355
+ for (const loaded of capabilities) {
356
+ if (!isUsableCapability(loaded))
357
+ continue;
358
+ choices.push(choiceForCapability("capability", formatResourceKey(loaded.key), loaded.capability, { kind: "capability", key: loaded.key }));
359
+ }
360
+ const bindings = readBindings(ctx);
361
+ if (!bindings.ok)
362
+ return { ok: false, status: 409, error: bindings.error };
363
+ for (const binding of bindings.bindings) {
364
+ choices.push(choiceForCapability("binding", binding.id, binding.capability, {
365
+ kind: "capability",
366
+ key: binding.ref,
367
+ binding: { preset: binding.preset, id: binding.id },
368
+ }));
369
+ }
370
+ const finalState = readState();
371
+ if (!finalState.ok)
372
+ return finalState;
373
+ if (!sameGuard(stateResult.state.guard, finalState.state.guard))
374
+ return { ok: false, status: 409, error: "Session changed during resource discovery. Refresh and review." };
375
+ return { ok: true, state: finalState.state, choices };
376
+ }
377
+ catch (error) {
378
+ return { ok: false, status: 503, error: `Capability choices unavailable: ${error instanceof Error ? error.message : String(error)}` };
379
+ }
380
+ }
381
+ /** Guarded, explicit human web activation. It never retries or infers. */
382
+ function enableCapability(input) {
383
+ if (!isCapabilityEnableRequest(input))
384
+ return { ok: false, status: 400, error: "Invalid capability activation payload." };
385
+ const initial = readState();
386
+ if (!initial.ok)
387
+ return initial;
388
+ if (disposed || initial.state.restoring)
389
+ return { ok: false, status: 503, error: "Capability session is restoring." };
390
+ if (!initial.state.trusted)
391
+ return { ok: false, status: 403, error: "Project is not trusted." };
392
+ if (!sameGuard(input.guard, initial.state.guard))
393
+ return { ok: false, status: 409, error: "Session, branch or capability state changed. Refresh and review before trying again." };
394
+ const ctx = context;
395
+ if (!ctx)
396
+ return { ok: false, status: 503, error: "No active Forge session." };
397
+ try {
398
+ let expected = "";
399
+ if (input.kind === "capability") {
400
+ const parsed = parseResourceSelector(input.id);
401
+ if (!parsed.ok || !parsed.selector.scope)
402
+ return { ok: false, status: 409, error: "Direct capability IDs must be qualified resource IDs." };
403
+ if (!ctx.isProjectTrusted())
404
+ return { ok: false, status: 403, error: "Project is not trusted." };
405
+ const capabilities = workspace.reloadCapabilities(ctx.cwd, ctx.isProjectTrusted()).capabilities;
406
+ const resolved = resolveCapability(createResourceCatalog([...capabilities]), input.id);
407
+ if (!resolved.ok)
408
+ return { ok: false, status: 409, error: resolved.error };
409
+ const capability = resolved.loaded.capability;
410
+ expected = choiceForCapability("capability", input.id, capability, { kind: "capability", key: resolved.loaded.key }).fingerprint;
411
+ if (expected !== input.fingerprint)
412
+ return { ok: false, status: 409, error: "Capability source changed. Refresh and review before trying again." };
413
+ }
414
+ else {
415
+ const bindings = readBindings(ctx);
416
+ if (!bindings.ok)
417
+ return { ok: false, status: 409, error: bindings.error };
418
+ const binding = bindings.bindings.find((candidate) => candidate.id === input.id);
419
+ if (!binding)
420
+ return { ok: false, status: 409, error: "Capability binding changed or is no longer available. Refresh and review before trying again." };
421
+ expected = choiceForCapability("binding", binding.id, binding.capability, {
422
+ kind: "capability", key: binding.ref, binding: { preset: binding.preset, id: binding.id },
423
+ }).fingerprint;
424
+ if (expected !== input.fingerprint)
425
+ return { ok: false, status: 409, error: "Capability binding changed. Refresh and review before trying again." };
426
+ }
427
+ const checked = readState();
428
+ if (!checked.ok)
429
+ return checked;
430
+ if (checked.state.restoring || !checked.state.trusted || !sameGuard(input.guard, checked.state.guard)) {
431
+ return { ok: false, status: checked.state.trusted ? 409 : 403, error: checked.state.trusted ? "Session, branch or capability state changed. Refresh and review before trying again." : "Project is not trusted." };
432
+ }
433
+ if (input.kind === "binding") {
434
+ const result = enableBound(ctx, input.id, "user", expected, input.guard);
435
+ if (!result.ok) {
436
+ try {
437
+ if (!ctx.isProjectTrusted())
438
+ return { ok: false, status: 403, error: result.error };
439
+ }
440
+ catch {
441
+ return { ok: false, status: 503, error: "Capability session is unavailable." };
442
+ }
443
+ return { ok: false, status: 409, error: result.error };
444
+ }
445
+ }
446
+ else {
447
+ change(ctx, "enable", input.id, expected, input.guard);
448
+ }
449
+ return readState();
450
+ }
451
+ catch (error) {
452
+ try {
453
+ if (!ctx.isProjectTrusted())
454
+ return { ok: false, status: 403, error: "Project is not trusted." };
455
+ }
456
+ catch {
457
+ return { ok: false, status: 503, error: "Capability session is unavailable." };
458
+ }
459
+ return { ok: false, status: 409, error: error instanceof Error ? error.message : String(error) };
460
+ }
461
+ }
462
+ /** Human Web controls share CLI semantics, with an exact displayed-view guard. */
463
+ function mutateState(input) {
464
+ if (!isCapabilityStateMutation(input))
465
+ return { ok: false, status: 400, error: "Invalid capability state operation." };
466
+ const current = readState();
467
+ if (!current.ok)
468
+ return current;
469
+ if (!current.state.trusted)
470
+ return { ok: false, status: 403, error: "Project is not trusted. Use the human CLI for recovery." };
471
+ const guard = current.state.guard;
472
+ if (current.state.restoring || input.guard.sessionId !== guard.sessionId || input.guard.leafId !== guard.leafId || input.guard.revision !== guard.revision) {
473
+ return { ok: false, status: 409, error: "Session, branch or capability state changed. Refresh and review before trying again." };
474
+ }
475
+ if (input.action === "disable" && !current.state.active.some((item) => item.activationId === input.activationId)) {
476
+ return { ok: false, status: 404, error: "No active capability with that activation ID." };
477
+ }
478
+ try {
479
+ // No await between checking the guard and committing to the current session.
480
+ change(context, input.action, input.action === "disable" ? input.activationId : "");
481
+ return readState();
482
+ }
483
+ catch (error) {
484
+ return { ok: false, status: 409, error: error instanceof Error ? error.message : String(error) };
485
+ }
486
+ }
487
+ function status(ctx, includeRuleContent = true) {
488
+ const { state } = view(ctx);
489
+ const presentation = ctx.model?.compat?.supportsMidConvoSystemMessages === true ? "native system sections" : "attributed user updates";
490
+ const delivery = !state.lastEventId ? "none" : preparedRevision === state.lastEventId && preparedModel === modelKey(ctx) ? "request prepared (not a model-obedience or delivery acknowledgment)" : "pending next request";
491
+ return [
492
+ `Capability capabilities: ${state.active.length}; ${presentation}; ${delivery}`,
493
+ `Currently selected tools: ${pi.getActiveTools().join(", ") || "(none)"}`,
494
+ "Tool transport is provider-managed: native text does not guarantee incremental tools. Full schema resubmission and cache changes are possible; prepared does not guarantee delivery or cache hits.",
495
+ ...state.active.flatMap((item) => [
496
+ `${item.snapshot.activationId} · ${sourceLabel(item)} · ${item.actor}`,
497
+ ` +tools: ${item.snapshot.tools.add.join(", ") || "(none)"}; -tools: ${item.snapshot.tools.remove.join(", ") || "(none)"}`,
498
+ ...(includeRuleContent ? [item.snapshot.content] : []),
499
+ ]),
500
+ ].join("\n");
501
+ }
502
+ function change(ctx, command, value, expectedFingerprint, expectedGuard) {
503
+ if (disposed || restoring || (context !== undefined && !sameContext(context, ctx)))
504
+ throw new Error("Capability runtime is no longer active for this session.");
505
+ context = ctx;
506
+ const { state } = view(ctx);
507
+ if ((command === "add" || command === "enable" || command === "enable-bound") && !ctx.isProjectTrusted())
508
+ throw new Error("Project is not trusted; refusing to activate an capability.");
509
+ const common = { schemaVersion: 1, eventId: randomUUID(), actor: "user", createdAt: Date.now() };
510
+ if (command === "reset") {
511
+ if (!state.active.length)
512
+ return "No active capabilities to reset.";
513
+ commit(ctx, { ...common, op: "reset" });
514
+ return "Capabilities stopped; tools recomputed. Historical messages and completed work are not undone.";
515
+ }
516
+ if (!value.trim())
517
+ throw new Error(`Usage: /capability ${command} <${command === "add" ? "text" : "id"}>`);
518
+ if (command === "enable-bound") {
519
+ const res = enableBound(ctx, value.trim(), "user");
520
+ if (!res.ok)
521
+ throw new Error(res.error);
522
+ return res.message;
523
+ }
524
+ if (command === "disable") {
525
+ const matches = state.active.filter((item) => item.snapshot.activationId === value || sourceLabel(item) === value
526
+ || (item.snapshot.source.kind === "capability" && item.snapshot.source.key.id === value)
527
+ || (item.snapshot.source.kind === "capability" && item.snapshot.source.binding && item.snapshot.source.binding.id === value));
528
+ if (!matches.length)
529
+ return "No matching active capability (already disabled or unknown ID).";
530
+ if (matches.length !== 1)
531
+ throw new Error("Ambiguous capability name; use the activation ID from /capability status.");
532
+ commit(ctx, { ...common, op: "deactivate", activationId: matches[0].snapshot.activationId });
533
+ return `Stopped ${matches[0].snapshot.activationId}; tools recomputed, stop notice pending next request.`;
534
+ }
535
+ let input;
536
+ if (command === "add") {
537
+ input = { activationId: randomUUID(), source: { kind: "manual" }, content: value, tools: { add: [], remove: [] } };
538
+ }
539
+ else {
540
+ const capabilities = workspace.reloadCapabilities(ctx.cwd, ctx.isProjectTrusted()).capabilities;
541
+ const resolved = resolveCapability(createResourceCatalog([...capabilities]), value);
542
+ if (!resolved.ok)
543
+ throw new Error(resolved.error);
544
+ assertActivationGuard(expectedGuard);
545
+ const existing = state.active.find((item) => item.snapshot.source.kind === "capability" && !item.snapshot.source.binding
546
+ && formatResourceKey(item.snapshot.source.key) === formatResourceKey(resolved.loaded.key));
547
+ if (existing)
548
+ return `Already selected as ${existing.snapshot.activationId}; source edits do not change this snapshot. Disable/enable to reapply.`;
549
+ const capability = resolved.loaded.capability;
550
+ input = { activationId: randomUUID(), source: { kind: "capability", key: resolved.loaded.key }, content: capability.content, tools: capability.tools,
551
+ ...(capability.name === undefined ? {} : { name: capability.name }) };
552
+ }
553
+ const snapshot = createCapabilitySnapshot(input);
554
+ if (expectedFingerprint !== undefined && snapshot.fingerprint !== expectedFingerprint) {
555
+ throw new Error("Capability source changed. Refresh and review before trying again.");
556
+ }
557
+ commit(ctx, { ...common, op: "activate", snapshot });
558
+ return selectedMessage(snapshot);
559
+ }
560
+ function readBindings(ctx) {
561
+ if (!ctx.isProjectTrusted()) {
562
+ return { ok: false, error: "Project is not trusted; cannot read capability bindings." };
563
+ }
564
+ const activePreset = workspace.snapshotKnown ? workspace.snapshot().active : undefined;
565
+ if (!activePreset) {
566
+ return { ok: true, preset: null, bindings: [] };
567
+ }
568
+ const declaredBindings = activePreset.stack.capabilities ?? [];
569
+ const presetMarker = fingerprintJson({ key: activePreset.key, stack: activePreset.stack });
570
+ if (declaredBindings.length === 0) {
571
+ return { ok: true, preset: activePreset.key, bindings: [] };
572
+ }
573
+ const capabilities = workspace.reloadCapabilities(ctx.cwd, ctx.isProjectTrusted()).capabilities;
574
+ const afterReload = workspace.snapshot().active;
575
+ if (!afterReload || fingerprintJson({ key: afterReload.key, stack: afterReload.stack }) !== presetMarker) {
576
+ return { ok: false, error: "Active preset changed while resolving capability bindings. Refresh and try again." };
577
+ }
578
+ const catalog = createResourceCatalog([...capabilities]);
579
+ const res = resolveCapabilityBindings(catalog, activePreset.key, declaredBindings);
580
+ if (!res.ok) {
581
+ return { ok: false, error: res.error };
582
+ }
583
+ return { ok: true, preset: activePreset.key, bindings: res.bindings };
584
+ }
585
+ function enableBound(ctx, id, actor = "user", expectedFingerprint, expectedGuard) {
586
+ if (disposed || restoring || (context !== undefined && !sameContext(context, ctx))) {
587
+ return { ok: false, error: "Capability runtime is no longer active for this session." };
588
+ }
589
+ context = ctx;
590
+ if (!ctx.isProjectTrusted()) {
591
+ return { ok: false, error: "Project is not trusted; refusing to activate an capability." };
592
+ }
593
+ if (typeof id !== "string" || !id.trim()) {
594
+ return { ok: false, error: "Binding ID must be a non-empty string." };
595
+ }
596
+ const targetId = id.trim();
597
+ if (targetId.length > MAX_ID_LENGTH) {
598
+ return { ok: false, error: `Binding ID must be at most ${MAX_ID_LENGTH} characters.` };
599
+ }
600
+ const activePreset = workspace.snapshotKnown ? workspace.snapshot().active : undefined;
601
+ if (!activePreset) {
602
+ return { ok: false, error: "No active preset; cannot activate a bound capability." };
603
+ }
604
+ const bindingsRes = readBindings(ctx);
605
+ if (!bindingsRes.ok) {
606
+ return { ok: false, error: bindingsRes.error };
607
+ }
608
+ const binding = bindingsRes.bindings.find((b) => b.id === targetId);
609
+ if (!binding) {
610
+ return {
611
+ ok: false,
612
+ error: `Capability "${targetId}" is not bound to active preset "${formatResourceKey(activePreset.key)}".`,
613
+ };
614
+ }
615
+ if (actor === "agent") {
616
+ if (!binding.modelCallable) {
617
+ return {
618
+ ok: false,
619
+ error: `Capability "${targetId}" is not authorized for agent use (modelCallable is not true).`,
620
+ };
621
+ }
622
+ if (binding.capability.tools.remove.includes("forge_capability")) {
623
+ return {
624
+ ok: false,
625
+ error: `Cannot activate capability "${targetId}": agent cannot remove control tool "forge_capability".`,
626
+ };
627
+ }
628
+ const allTools = pi.getAllTools();
629
+ const activeTools = pi.getActiveTools();
630
+ if (!allTools.some((t) => t.name === "forge_capability") ||
631
+ !activeTools.includes("forge_capability") ||
632
+ tools.blockReason("forge_capability") !== undefined) {
633
+ return { ok: false, error: 'Control tool "forge_capability" is not available or blocked by policy.' };
634
+ }
635
+ }
636
+ const policyErr = tools.validateCapabilities([binding.capability.tools]);
637
+ if (policyErr) {
638
+ return { ok: false, error: `Cannot activate capability "${targetId}": ${policyErr}` };
639
+ }
640
+ try {
641
+ assertActivationGuard(expectedGuard);
642
+ }
643
+ catch (error) {
644
+ return { ok: false, error: String(error) };
645
+ }
646
+ // Repeated use idempotent/no owner takeover
647
+ const { state } = view(ctx);
648
+ const existing = state.active.find((item) => item.snapshot.source.kind === "capability" &&
649
+ item.snapshot.source.binding &&
650
+ formatResourceKey(item.snapshot.source.binding.preset) === formatResourceKey(binding.preset) &&
651
+ item.snapshot.source.binding.id === binding.id &&
652
+ formatResourceKey(item.snapshot.source.key) === formatResourceKey(binding.ref));
653
+ const stale = state.active.find((item) => item.snapshot.source.kind === "capability" &&
654
+ item.snapshot.source.binding &&
655
+ formatResourceKey(item.snapshot.source.binding.preset) === formatResourceKey(binding.preset) &&
656
+ item.snapshot.source.binding.id === binding.id);
657
+ if (stale && !existing) {
658
+ return {
659
+ ok: false,
660
+ error: `Capability "${targetId}" has a stale binding identity; disable the old activation before reusing it.`,
661
+ };
662
+ }
663
+ if (existing) {
664
+ return {
665
+ ok: true,
666
+ activationId: existing.snapshot.activationId,
667
+ idempotent: true,
668
+ message: `Capability "${targetId}" is already active as ${existing.snapshot.activationId}. Repeated use is idempotent and does not change ownership.`,
669
+ };
670
+ }
671
+ const snapshot = createCapabilitySnapshot({
672
+ activationId: randomUUID(),
673
+ source: {
674
+ kind: "capability",
675
+ key: binding.ref,
676
+ binding: {
677
+ preset: binding.preset,
678
+ id: binding.id,
679
+ },
680
+ },
681
+ content: binding.capability.content,
682
+ tools: binding.capability.tools,
683
+ ...(binding.capability.name !== undefined ? { name: binding.capability.name } : {}),
684
+ });
685
+ if (expectedFingerprint !== undefined && snapshot.fingerprint !== expectedFingerprint) {
686
+ return { ok: false, error: "Capability source or binding changed. Refresh and review before trying again." };
687
+ }
688
+ try {
689
+ commit(ctx, {
690
+ schemaVersion: 1,
691
+ eventId: randomUUID(),
692
+ op: "activate",
693
+ actor,
694
+ createdAt: Date.now(),
695
+ snapshot,
696
+ });
697
+ }
698
+ catch (err) {
699
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
700
+ }
701
+ return {
702
+ ok: true,
703
+ activationId: snapshot.activationId,
704
+ message: selectedMessage(snapshot),
705
+ };
706
+ }
707
+ function selectedMessage(snapshot) {
708
+ const delivery = snapshot.content.trim().length === 0
709
+ ? "tools prepared; tool-only capability, no instruction text is sent."
710
+ : "tools prepared, instruction pending next model request.";
711
+ return `Selected ${snapshot.activationId}; ${delivery} Use /capability disable ${snapshot.activationId} to stop.`;
712
+ }
713
+ function disableBound(ctx, id, actor = "user") {
714
+ if (disposed || restoring || (context !== undefined && !sameContext(context, ctx))) {
715
+ return { ok: false, error: "Capability runtime is no longer active for this session." };
716
+ }
717
+ context = ctx;
718
+ if (!ctx.isProjectTrusted()) {
719
+ return { ok: false, error: "Project is not trusted; refusing to deactivate an capability." };
720
+ }
721
+ if (typeof id !== "string" || !id.trim()) {
722
+ return { ok: false, error: "Invalid capability id." };
723
+ }
724
+ const targetId = id.trim();
725
+ if (targetId.length > MAX_ID_LENGTH) {
726
+ return { ok: false, error: `Capability ID must be at most ${MAX_ID_LENGTH} characters.` };
727
+ }
728
+ const activePreset = workspace.snapshotKnown ? workspace.snapshot().active : undefined;
729
+ if (actor === "agent") {
730
+ const allTools = pi.getAllTools();
731
+ const activeTools = pi.getActiveTools();
732
+ if (!allTools.some((t) => t.name === "forge_capability") ||
733
+ !activeTools.includes("forge_capability") ||
734
+ tools.blockReason("forge_capability") !== undefined) {
735
+ return { ok: false, error: 'Control tool "forge_capability" is not available or blocked by policy.' };
736
+ }
737
+ if (!activePreset) {
738
+ return { ok: false, error: "forge_capability disable requires an active preset with bound capabilities." };
739
+ }
740
+ }
741
+ const { state } = view(ctx);
742
+ const matches = state.active.filter((item) => item.snapshot.activationId === targetId ||
743
+ sourceLabel(item) === targetId ||
744
+ (item.snapshot.source.kind === "capability" && item.snapshot.source.key.id === targetId) ||
745
+ (item.snapshot.source.kind === "capability" &&
746
+ item.snapshot.source.binding &&
747
+ item.snapshot.source.binding.id === targetId));
748
+ if (!matches.length) {
749
+ return { ok: false, error: `No matching active capability: ${targetId}` };
750
+ }
751
+ if (matches.length > 1) {
752
+ return { ok: false, error: `Ambiguous active capability "${targetId}"; use its activation ID.` };
753
+ }
754
+ const target = matches[0];
755
+ if (actor === "agent") {
756
+ if (target.actor !== "agent") {
757
+ return { ok: false, error: `Agent cannot deactivate user-owned activation: ${target.snapshot.activationId}` };
758
+ }
759
+ const source = target.snapshot.source;
760
+ if (source.kind !== "capability" || !source.binding) {
761
+ return { ok: false, error: `Agent cannot deactivate unbound or manual capability: ${target.snapshot.activationId}` };
762
+ }
763
+ // Current authorization required
764
+ const bindingsRes = readBindings(ctx);
765
+ if (!bindingsRes.ok) {
766
+ return { ok: false, error: `Failed to resolve active preset bindings: ${bindingsRes.error}` };
767
+ }
768
+ const currentBinding = bindingsRes.bindings.find((b) => b.id === source.binding.id);
769
+ const samePreset = formatResourceKey(source.binding.preset) === formatResourceKey(activePreset.key);
770
+ const sameCapability = currentBinding && formatResourceKey(source.key) === formatResourceKey(currentBinding.ref);
771
+ if (!currentBinding || !samePreset || !sameCapability || !currentBinding.modelCallable) {
772
+ return {
773
+ ok: false,
774
+ error: `Cannot deactivate capability "${targetId}": current authorization requires the exact active-preset binding identity (including its capability source) and modelCallable authorization.`,
775
+ };
776
+ }
777
+ }
778
+ try {
779
+ commit(ctx, {
780
+ schemaVersion: 1,
781
+ eventId: randomUUID(),
782
+ op: "deactivate",
783
+ actor,
784
+ createdAt: Date.now(),
785
+ activationId: target.snapshot.activationId,
786
+ });
787
+ }
788
+ catch (err) {
789
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
790
+ }
791
+ return {
792
+ ok: true,
793
+ activationId: target.snapshot.activationId,
794
+ message: `Stopped ${target.snapshot.activationId}; tools recomputed, stop notice pending next request.`,
795
+ };
796
+ }
797
+ async function executeAgentTool(ctx, params) {
798
+ if (disposed || restoring) {
799
+ throw new Error("forge_capability is unavailable for this session.");
800
+ }
801
+ if (!isPlainObject(params)) {
802
+ throw new Error("forge_capability parameters must be a plain object.");
803
+ }
804
+ const allowedKeys = new Set(["action", "id"]);
805
+ for (const key of Object.keys(params)) {
806
+ if (!allowedKeys.has(key)) {
807
+ throw new Error(`forge_capability: unknown argument "${key}". Only "action" and "id" are permitted.`);
808
+ }
809
+ }
810
+ const action = params.action;
811
+ if (action !== "list" && action !== "status" && action !== "enable" && action !== "disable") {
812
+ throw new Error(`forge_capability: invalid action "${String(action)}". Expected "list", "status", "enable", or "disable".`);
813
+ }
814
+ if (action === "list" || action === "status") {
815
+ if (params.id !== undefined) {
816
+ throw new Error(`forge_capability: argument "id" is irrelevant for action "${action}".`);
817
+ }
818
+ }
819
+ if (action === "enable" || action === "disable") {
820
+ if (typeof params.id !== "string" || !params.id.trim()) {
821
+ throw new Error(`forge_capability: argument "id" is required for action "${action}".`);
822
+ }
823
+ if (params.id.trim().length > MAX_ID_LENGTH) {
824
+ throw new Error(`forge_capability: argument "id" must be at most ${MAX_ID_LENGTH} characters.`);
825
+ }
826
+ }
827
+ // EACH call checks: current trust, control-tool availability, active preset
828
+ if (!ctx.isProjectTrusted()) {
829
+ throw new Error("forge_capability requires a trusted project.");
830
+ }
831
+ if (context !== undefined && !sameContext(context, ctx))
832
+ throw new Error("forge_capability is unavailable for this session.");
833
+ context = ctx;
834
+ const allTools = pi.getAllTools();
835
+ const activeTools = pi.getActiveTools();
836
+ if (!allTools.some((t) => t.name === "forge_capability") ||
837
+ !activeTools.includes("forge_capability") ||
838
+ tools.blockReason("forge_capability") !== undefined) {
839
+ throw new Error('Control tool "forge_capability" is not available or blocked by policy.');
840
+ }
841
+ const activePreset = workspace.snapshotKnown ? workspace.snapshot().active : undefined;
842
+ if (!activePreset) {
843
+ throw new Error("forge_capability requires an active preset with bound capabilities.");
844
+ }
845
+ if (action === "list") {
846
+ const bindingsRes = readBindings(ctx);
847
+ if (!bindingsRes.ok)
848
+ throw new Error(bindingsRes.error);
849
+ const eligible = bindingsRes.bindings.filter((b) => {
850
+ if (!b.modelCallable)
851
+ return false;
852
+ if (b.capability.tools.remove.includes("forge_capability"))
853
+ return false;
854
+ if (tools.validateCapabilities([b.capability.tools]) !== undefined)
855
+ return false;
856
+ return true;
857
+ });
858
+ const text = eligible.length
859
+ ? eligible
860
+ .map((b) => [
861
+ `id: ${b.id}${b.capability.name ? ` (${b.capability.name})` : ""}`,
862
+ b.capability.description ? ` description: ${b.capability.description}` : undefined,
863
+ ` tools: +${b.capability.tools.add.join(", ") || "(none)"}; -${b.capability.tools.remove.join(", ") || "(none)"}`,
864
+ ]
865
+ .filter(Boolean)
866
+ .join("\n"))
867
+ .join("\n\n")
868
+ : "No authorized capabilities available for agent.";
869
+ return {
870
+ content: [{ type: "text", text }],
871
+ details: {
872
+ action: "list",
873
+ capabilities: eligible.map((b) => ({ id: b.id, name: b.capability.name, tools: b.capability.tools })),
874
+ },
875
+ };
876
+ }
877
+ if (action === "status") {
878
+ const { state } = view(ctx);
879
+ const text = status(ctx, false);
880
+ return {
881
+ content: [{ type: "text", text }],
882
+ details: {
883
+ action: "status",
884
+ activeCount: state.active.length,
885
+ active: state.active.map((item) => ({
886
+ activationId: item.snapshot.activationId,
887
+ actor: item.actor,
888
+ source: sourceLabel(item),
889
+ bindingId: item.snapshot.source.kind === "capability" && item.snapshot.source.binding
890
+ ? item.snapshot.source.binding.id
891
+ : undefined,
892
+ tools: item.snapshot.tools,
893
+ })),
894
+ },
895
+ };
896
+ }
897
+ if (action === "enable") {
898
+ const res = enableBound(ctx, params.id, "agent");
899
+ if (!res.ok) {
900
+ throw new Error(res.error);
901
+ }
902
+ return {
903
+ content: [{ type: "text", text: res.message }],
904
+ details: { action: "enable", activationId: res.activationId, idempotent: res.idempotent === true },
905
+ };
906
+ }
907
+ if (action === "disable") {
908
+ const res = disableBound(ctx, params.id, "agent");
909
+ if (!res.ok) {
910
+ throw new Error(res.error);
911
+ }
912
+ return {
913
+ content: [{ type: "text", text: res.message }],
914
+ details: { action: "disable", activationId: res.activationId },
915
+ };
916
+ }
917
+ throw new Error(`Unhandled action: ${action}`);
918
+ }
919
+ function commitEndAnchors(ctx) {
920
+ context = ctx;
921
+ if (restoring)
922
+ return;
923
+ if (!pendingEvents(ctx).length)
924
+ return;
925
+ // Interrupted batches may leave an incomplete current tail. Keep intent
926
+ // pending rather than anchoring between its call and a later result.
927
+ if (hasPendingCapabilityToolCalls(canonicalSessionMessages(ctx)))
928
+ return;
929
+ persistPendingAnchors(ctx);
930
+ }
931
+ function assertActivationGuard(expected) {
932
+ if (!expected)
933
+ return;
934
+ const current = readState();
935
+ if (disposed || !current.ok || current.state.restoring || !current.state.trusted || !sameGuard(expected, current.state.guard)) {
936
+ throw new Error("Session changed during resource resolution. Refresh and review before activating.");
937
+ }
938
+ }
939
+ function sameGuard(a, b) {
940
+ return a.sessionId === b.sessionId && a.leafId === b.leafId && a.revision === b.revision;
941
+ }
942
+ function sameContext(a, b) {
943
+ try {
944
+ if (!a.sessionManager || !b.sessionManager)
945
+ return false;
946
+ return a.cwd === b.cwd && a.sessionManager.getSessionId() === b.sessionManager.getSessionId()
947
+ && a.sessionManager.getLeafId() === b.sessionManager.getLeafId();
948
+ }
949
+ catch {
950
+ return false;
951
+ }
952
+ }
953
+ function dispose() {
954
+ disposed = true;
955
+ context = undefined;
956
+ restoring = true;
957
+ preparedRevision = undefined;
958
+ preparedModel = undefined;
959
+ restoredTools = undefined;
960
+ agentBusy = false;
961
+ }
962
+ return { prepareRestore, restore, sync, prepareMessages, project, commitEndAnchors, setAgentBusy, library, completionView, status, change, readBindings, enableBound, disableBound, executeAgentTool, readState, mutateState, readAvailableCapabilities, enableCapability, dispose };
963
+ }
964
+ function sourceLabel(item) {
965
+ return item.snapshot.source.kind === "manual" ? "manual" : formatResourceKey(item.snapshot.source.key);
966
+ }
967
+ //# sourceMappingURL=capability-runtime.js.map