chainlesschain 0.166.140 → 0.166.142

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.
@@ -0,0 +1,28 @@
1
+ /** Implemented CLI surfaces; source leases and per-call activation still gate use. */
2
+ import { VERSION } from "../../constants.js";
3
+
4
+ export const CLI_GOAL_CAPABILITIES = Object.freeze([
5
+ "event:goal.status.changed",
6
+ "event:risk.review.completed",
7
+ "event:action.receipt.updated",
8
+ "panel:goal.summary",
9
+ "panel:goal.evidence",
10
+ "proposal:task.description.update",
11
+ ]);
12
+
13
+ export function cliGoalExtensionHost(activeCapabilities) {
14
+ if (
15
+ !Array.isArray(activeCapabilities) ||
16
+ activeCapabilities.some(
17
+ (value) => !CLI_GOAL_CAPABILITIES.includes(value),
18
+ ) ||
19
+ new Set(activeCapabilities).size !== activeCapabilities.length
20
+ )
21
+ throw new Error("CLI_GOAL_HOST_CAPABILITIES_INVALID");
22
+ return Object.freeze({
23
+ id: "cli",
24
+ version: VERSION,
25
+ capabilities: CLI_GOAL_CAPABILITIES,
26
+ activeCapabilities: Object.freeze([...activeCapabilities]),
27
+ });
28
+ }
@@ -1,15 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { VERSION } from "../../constants.js";
3
+ import { cliGoalExtensionHost } from "./cli-goal-capabilities.js";
4
4
  import { createProjectGoalConnection } from "../project-goal-connection.js";
5
5
  import { collectGoalExtensions } from "./goal-extensions.js";
6
6
  import { createGoalDraftRequest } from "@chainlesschain/session-core/goal-draft-request-contract";
7
7
  import { digestBusinessObjectContent as digest } from "@chainlesschain/session-core/business-object-contract";
8
- const HOST = Object.freeze({
9
- id: "cli",
10
- version: VERSION,
11
- capabilities: Object.freeze(["proposal:task.description.update"]),
12
- });
8
+ const HOST = cliGoalExtensionHost(["proposal:task.description.update"]);
13
9
  function fail(code) {
14
10
  throw new Error(code);
15
11
  }
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { VERSION } from "../../constants.js";
3
+ import { cliGoalExtensionHost } from "./cli-goal-capabilities.js";
4
4
  import { managedSettingsPath } from "../settings-loader.cjs";
5
5
  import { createProjectGoalConnection } from "../project-goal-connection.js";
6
6
  import {
@@ -20,11 +20,7 @@ import {
20
20
  validateGoalDraftSubmissionReference,
21
21
  } from "@chainlesschain/session-core/goal-draft-connection-contract";
22
22
 
23
- const HOST = Object.freeze({
24
- id: "cli",
25
- version: VERSION,
26
- capabilities: Object.freeze(["proposal:task.description.update"]),
27
- });
23
+ const HOST = cliGoalExtensionHost(["proposal:task.description.update"]);
28
24
  export const CLI_GOAL_DRAFT_SUBMISSION_SCHEMA =
29
25
  "chainlesschain.goal-draft-submission/v1";
30
26
  export const CLI_GOAL_DRAFT_SOURCES_SCHEMA =
@@ -0,0 +1,284 @@
1
+ /** One acknowledged page at a time; checkpoints contain no credentials or bodies. */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { cliGoalExtensionHost } from "./cli-goal-capabilities.js";
6
+ import { managedSettingsPath } from "../settings-loader.cjs";
7
+ import { withFileLockAsync } from "../with-file-lock.js";
8
+ import { createProjectGoalConnection } from "../project-goal-connection.js";
9
+ import {
10
+ collectGoalExtensions,
11
+ projectGoalExtensionEvent,
12
+ } from "./goal-extensions.js";
13
+ import { parseGoalEventCursor } from "@chainlesschain/session-core/goal-evidence-contract";
14
+ const TYPES = ["risk.review.completed", "action.receipt.updated"];
15
+ const hash = (value) =>
16
+ createHash("sha256").update(JSON.stringify(value)).digest("hex");
17
+ const fail = (code) => {
18
+ throw Object.assign(new Error(code), { code });
19
+ };
20
+ export const _deps = { createProjectGoalConnection, projectGoalExtensionEvent };
21
+ const sameFile = (a, b) =>
22
+ a.dev === b.dev &&
23
+ a.ino === b.ino &&
24
+ a.size === b.size &&
25
+ a.mtimeMs === b.mtimeMs &&
26
+ a.ctimeMs === b.ctimeMs;
27
+ function readFile(file) {
28
+ const entry = fs.lstatSync(file);
29
+ if (
30
+ !entry.isFile() ||
31
+ entry.isSymbolicLink() ||
32
+ entry.nlink !== 1 ||
33
+ entry.size > 8192
34
+ )
35
+ fail("GOAL_EVENT_CHECKPOINT_INVALID");
36
+ const fd = fs.openSync(
37
+ file,
38
+ fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0),
39
+ );
40
+ try {
41
+ if (!sameFile(entry, fs.fstatSync(fd)))
42
+ fail("GOAL_EVENT_CHECKPOINT_CHANGED");
43
+ const bytes = Buffer.alloc(entry.size + 1),
44
+ n = fs.readSync(fd, bytes, 0, bytes.length, 0);
45
+ if (
46
+ n !== entry.size ||
47
+ !sameFile(entry, fs.fstatSync(fd)) ||
48
+ !sameFile(entry, fs.lstatSync(file))
49
+ )
50
+ fail("GOAL_EVENT_CHECKPOINT_CHANGED");
51
+ const body = bytes.subarray(0, n).toString("utf8");
52
+ return { record: JSON.parse(body), digest: hash(body) };
53
+ } finally {
54
+ fs.closeSync(fd);
55
+ }
56
+ }
57
+ /** Publish must acknowledge flushed output; a crash before checkpoint commit replays. */
58
+ export function replayCliGoalEvents(id, raw = {}, publish) {
59
+ const cwd = path.resolve(raw.cwd || process.cwd()),
60
+ env = Object.freeze({ ...(raw.env || process.env) });
61
+ const required = (value) => {
62
+ if (
63
+ typeof value !== "string" ||
64
+ !value ||
65
+ value.length > 4096 ||
66
+ /[\u0000-\u001f]/u.test(value)
67
+ )
68
+ fail("GOAL_EVENT_ARGUMENT_REQUIRED");
69
+ return value;
70
+ };
71
+ const connection = path.resolve(cwd, required(raw.connection)),
72
+ checkpoint = path.resolve(cwd, required(raw.checkpoint));
73
+ const name = required(raw.plugin),
74
+ scope = raw.scope,
75
+ expectedStore = raw.expectedStore;
76
+ const signal = raw.signal;
77
+ const checkpointMode = raw.checkpointMode ?? "auto";
78
+ if (!["auto", "new", "resume"].includes(checkpointMode))
79
+ fail("GOAL_EVENT_ARGUMENT_REQUIRED");
80
+ if (
81
+ !["user", "project", "local"].includes(scope) ||
82
+ typeof publish !== "function"
83
+ )
84
+ fail("GOAL_EVENT_ARGUMENT_REQUIRED");
85
+ const policyEnv = { ...env };
86
+ if (policyEnv.CC_MANAGED_SETTINGS)
87
+ policyEnv.CC_MANAGED_SETTINGS = path.resolve(
88
+ cwd,
89
+ required(policyEnv.CC_MANAGED_SETTINGS),
90
+ );
91
+ const policyFile = managedSettingsPath({
92
+ env: policyEnv,
93
+ managedSettingsFile:
94
+ raw.managedSettingsFile === undefined
95
+ ? undefined
96
+ : path.resolve(cwd, required(raw.managedSettingsFile)),
97
+ });
98
+ const options = Object.freeze({
99
+ cwd,
100
+ env,
101
+ managedSettingsFile: policyFile,
102
+ host: cliGoalExtensionHost(TYPES.map((type) => `event:${type}`)),
103
+ });
104
+ return (async () => {
105
+ const source = _deps.createProjectGoalConnection(connection);
106
+ if (!source.eventHistory || typeof source.readEventPage !== "function")
107
+ fail("GOAL_EVENT_HISTORY_CONNECTION_REQUIRED");
108
+ const goalRef = source.goalRef;
109
+ if (
110
+ goalRef.id !== id ||
111
+ (expectedStore !== undefined && expectedStore !== goalRef.storeId)
112
+ )
113
+ fail("CLI_GOAL_STORE_MISMATCH");
114
+ const selected = collectGoalExtensions(options).extensions.find(
115
+ (entry) =>
116
+ entry.name === name &&
117
+ entry.scope === scope &&
118
+ entry.declaration.events.some((type) => TYPES.includes(type)),
119
+ );
120
+ if (!selected) fail("CLI_GOAL_EVENT_PLUGIN_UNAVAILABLE");
121
+ const plugin = Object.freeze({
122
+ name: selected.name,
123
+ scope: selected.scope,
124
+ version: selected.version,
125
+ }),
126
+ declarationDigest = hash(selected.declaration);
127
+ function admission() {
128
+ if (signal?.aborted) fail("GOAL_EVENT_REPLAY_ABORTED");
129
+ const current = collectGoalExtensions(options).extensions.find(
130
+ (entry) =>
131
+ entry.name === name &&
132
+ entry.scope === scope &&
133
+ entry.version === plugin.version,
134
+ );
135
+ if (!current || hash(current.declaration) !== declarationDigest)
136
+ fail("CLI_GOAL_EVENT_ADMISSION_CHANGED");
137
+ }
138
+ admission();
139
+ const root = path.dirname(checkpoint),
140
+ realRoot = fs.realpathSync(root),
141
+ rootStat = fs.statSync(root);
142
+ const checkRoot = () => {
143
+ const stat = fs.statSync(root);
144
+ if (
145
+ fs.realpathSync(root) !== realRoot ||
146
+ !stat.isDirectory() ||
147
+ stat.dev !== rootStat.dev ||
148
+ stat.ino !== rootStat.ino
149
+ )
150
+ fail("GOAL_EVENT_CHECKPOINT_CHANGED");
151
+ };
152
+ const binding = {
153
+ schema: "chainlesschain.goal-event-checkpoint/v1",
154
+ goalRef,
155
+ plugin,
156
+ declarationDigest,
157
+ };
158
+ return withFileLockAsync(
159
+ checkpoint,
160
+ async (lock) => {
161
+ checkRoot();
162
+ admission();
163
+ const exists = fs.existsSync(checkpoint);
164
+ if (checkpointMode === "new" && exists)
165
+ fail("GOAL_EVENT_CHECKPOINT_EXISTS");
166
+ if (checkpointMode === "resume" && !exists)
167
+ fail("GOAL_EVENT_CHECKPOINT_MISSING");
168
+ if (!fs.existsSync(checkpoint)) {
169
+ const fd = fs.openSync(checkpoint, "wx", 0o600);
170
+ try {
171
+ fs.writeFileSync(fd, JSON.stringify({ ...binding, after: null }));
172
+ fs.fsyncSync(fd);
173
+ } finally {
174
+ fs.closeSync(fd);
175
+ }
176
+ }
177
+ const initial = readFile(checkpoint),
178
+ record = initial.record;
179
+ if (
180
+ !record ||
181
+ Object.keys(record).sort().join() !==
182
+ "after,declarationDigest,goalRef,plugin,schema" ||
183
+ hash({ ...record, after: null }) !== hash({ ...binding, after: null })
184
+ )
185
+ fail("GOAL_EVENT_CHECKPOINT_BINDING_CONFLICT");
186
+ if (record.after !== null) parseGoalEventCursor(record.after);
187
+ const after = record.after ?? undefined;
188
+ const page = await source.readEventPage(after, { signal });
189
+ admission();
190
+ const events = [];
191
+ for (const entry of page.entries) {
192
+ if (!selected.declaration.events.includes(entry.type)) continue;
193
+ const summary =
194
+ entry.type === "risk.review.completed"
195
+ ? `Historical risk check ${entry.sourceId}: ${entry.state}; risk task count ${entry.riskTaskCount ?? "unknown"}. This does not prove current risk or goal completion.`
196
+ : `Historical action receipt ${entry.sourceId}: ${entry.state} (${entry.actionType}). This does not prove goal completion.`;
197
+ const event = await _deps.projectGoalExtensionEvent(
198
+ plugin,
199
+ { type: entry.type, goalRef },
200
+ {
201
+ ...options,
202
+ authorize(context) {
203
+ admission();
204
+ return (
205
+ context.operation === "read-event" &&
206
+ context.type === entry.type &&
207
+ hash(context.goalRef) === hash(goalRef)
208
+ );
209
+ },
210
+ readEvent() {
211
+ return { summary, occurredAt: entry.recordedAt };
212
+ },
213
+ },
214
+ );
215
+ events.push({
216
+ id: entry.id,
217
+ sequence: entry.sequence,
218
+ sourceRef: entry.sourceRef,
219
+ recordedAt: entry.recordedAt,
220
+ event,
221
+ });
222
+ }
223
+ async function revalidate() {
224
+ admission();
225
+ checkRoot();
226
+ if (readFile(checkpoint).digest !== initial.digest)
227
+ fail("GOAL_EVENT_CHECKPOINT_CHANGED");
228
+ const current = await source.readEventPage(after, { signal });
229
+ if (
230
+ hash(current.entries.slice(0, page.entries.length)) !==
231
+ hash(page.entries) ||
232
+ (page.nextCursor === null && current.nextCursor !== null) ||
233
+ (page.nextCursor &&
234
+ parseGoalEventCursor(page.nextCursor).epoch !==
235
+ parseGoalEventCursor(current.nextCursor).epoch)
236
+ )
237
+ fail("GOAL_EVENT_HISTORY_CHANGED");
238
+ admission();
239
+ }
240
+ await revalidate();
241
+ const result = {
242
+ schema: "chainlesschain.goal-extension-history/v1",
243
+ goalRef,
244
+ plugin,
245
+ observedAt: new Date().toISOString(),
246
+ delivery: "at-least-once",
247
+ entries: events,
248
+ scannedCount: page.entries.length,
249
+ nextCursor: page.nextCursor,
250
+ hasMore: page.hasMore,
251
+ truncated: page.truncated,
252
+ retainedFrom: page.retainedFrom,
253
+ highWatermark: page.highWatermark,
254
+ };
255
+ if ((await publish(Object.freeze(result))) !== true)
256
+ fail("GOAL_EVENT_OUTPUT_NOT_ACKNOWLEDGED");
257
+ await revalidate();
258
+ lock.assertOwnership();
259
+ const temporary = `${checkpoint}.${randomUUID()}.tmp`;
260
+ try {
261
+ const fd = fs.openSync(temporary, "wx", 0o600);
262
+ try {
263
+ fs.writeFileSync(
264
+ fd,
265
+ JSON.stringify({ ...binding, after: page.nextCursor }),
266
+ );
267
+ fs.fsyncSync(fd);
268
+ } finally {
269
+ fs.closeSync(fd);
270
+ }
271
+ checkRoot();
272
+ lock.assertOwnership();
273
+ if (readFile(checkpoint).digest !== initial.digest)
274
+ fail("GOAL_EVENT_CHECKPOINT_CHANGED");
275
+ fs.renameSync(temporary, checkpoint);
276
+ } finally {
277
+ if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
278
+ }
279
+ return { checkpoint, hasMore: page.hasMore };
280
+ },
281
+ { failIfUnavailable: true, timeoutMs: 2000 },
282
+ );
283
+ })();
284
+ }
@@ -0,0 +1,175 @@
1
+ /** Explicit, bounded polling of standalone goal status. No durable event log. */
2
+ import path from "node:path";
3
+ import { performance } from "node:perf_hooks";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { cliGoalExtensionHost } from "./cli-goal-capabilities.js";
6
+ import { resolveConfigDataRoot } from "../paths.js";
7
+ import { managedSettingsPath } from "../settings-loader.cjs";
8
+ import { createCliGoalSnapshotReader } from "../cli-goal-source.js";
9
+ import {
10
+ collectGoalExtensions,
11
+ projectGoalExtensionEvent,
12
+ } from "./goal-extensions.js";
13
+
14
+ const TYPE = "goal.status.changed";
15
+ export const CLI_GOAL_EVENT_SCHEMA = "chainlesschain.goal-extension-stream/v1";
16
+ export const _deps = { projectGoalExtensionEvent };
17
+ const fail = (code) => {
18
+ throw new Error(code);
19
+ };
20
+
21
+ /** Capture selection synchronously; retained iterators never change workspace. */
22
+ export function watchCliGoalExtensionEvents(id, options = {}) {
23
+ const cwd = path.resolve(options.cwd || process.cwd());
24
+ const env = Object.freeze({ ...(options.env || process.env) });
25
+ const root = path.resolve(
26
+ options.root ||
27
+ path.join(resolveConfigDataRoot({ cwd, env }).path, "goals"),
28
+ );
29
+ const managedSettingsFile = managedSettingsPath({
30
+ env,
31
+ managedSettingsFile: options.managedSettingsFile,
32
+ });
33
+ const name = options.plugin,
34
+ scope = options.scope;
35
+ const expectedStore = options.expectedStore;
36
+ const intervalMs = options.intervalMs ?? 1000;
37
+ const durationMs = options.durationMs ?? 60000;
38
+ const signal = options.signal;
39
+ if (options.connection !== undefined) fail("CLI_GOAL_EVENTS_STANDALONE_ONLY");
40
+ if (typeof name !== "string" || !name || name.length > 256)
41
+ fail("CLI_GOAL_INVALID_PLUGIN_NAME");
42
+ if (scope !== undefined && !["user", "project", "local"].includes(scope))
43
+ fail("CLI_GOAL_INVALID_PLUGIN_SCOPE");
44
+ if (
45
+ !Number.isSafeInteger(intervalMs) ||
46
+ intervalMs < 250 ||
47
+ intervalMs > 60000 ||
48
+ !Number.isSafeInteger(durationMs) ||
49
+ durationMs < 1000 ||
50
+ durationMs > 900000
51
+ )
52
+ fail("CLI_GOAL_INVALID_WATCH_LIMIT");
53
+ if (signal !== undefined && !(signal instanceof AbortSignal))
54
+ fail("CLI_GOAL_INVALID_WATCH_SIGNAL");
55
+ const host = cliGoalExtensionHost([`event:${TYPE}`]);
56
+ const admission = Object.freeze({ cwd, env, managedSettingsFile, host });
57
+
58
+ return (async function* () {
59
+ if (signal?.aborted) return;
60
+ const source = createCliGoalSnapshotReader(root);
61
+ if (expectedStore !== undefined && expectedStore !== source.storeId)
62
+ fail("CLI_GOAL_STORE_MISMATCH");
63
+ const matches = collectGoalExtensions(admission).extensions.filter(
64
+ (entry) =>
65
+ entry.name === name &&
66
+ (!scope || entry.scope === scope) &&
67
+ entry.declaration.events.includes(TYPE),
68
+ );
69
+ if (matches.length !== 1) fail("CLI_GOAL_EVENT_PLUGIN_UNAVAILABLE");
70
+ const plugin = matches[0];
71
+ const ref = Object.freeze({
72
+ name: plugin.name,
73
+ scope: plugin.scope,
74
+ version: plugin.version,
75
+ });
76
+ const declaration = JSON.stringify(plugin.declaration);
77
+ function revalidatePlugin() {
78
+ const current = collectGoalExtensions(admission).extensions.find(
79
+ (entry) =>
80
+ entry.name === ref.name &&
81
+ entry.scope === ref.scope &&
82
+ entry.version === ref.version,
83
+ );
84
+ if (!current || JSON.stringify(current.declaration) !== declaration)
85
+ fail("CLI_GOAL_EVENT_ADMISSION_CHANGED");
86
+ }
87
+ const reference = (goal) =>
88
+ Object.freeze({
89
+ id: goal.id,
90
+ storeId: goal.storeId,
91
+ revision: goal.revision,
92
+ controlGeneration: goal.controlGeneration,
93
+ });
94
+ let snapshot = source.read(id);
95
+ source.revalidate(snapshot);
96
+ revalidatePlugin();
97
+ const deadline = performance.now() + durationMs;
98
+ yield Object.freeze({
99
+ schema: CLI_GOAL_EVENT_SCHEMA,
100
+ kind: "ready",
101
+ plugin: ref,
102
+ goalRef: reference(snapshot.goal),
103
+ observedAt: new Date().toISOString(),
104
+ delivery: "polling",
105
+ replay: false,
106
+ });
107
+ while (!signal?.aborted && performance.now() < deadline) {
108
+ try {
109
+ await delay(
110
+ Math.min(intervalMs, Math.max(1, deadline - performance.now())),
111
+ undefined,
112
+ { signal },
113
+ );
114
+ } catch (error) {
115
+ if (signal?.aborted && error.name === "AbortError") return;
116
+ throw error;
117
+ }
118
+ if (signal?.aborted || performance.now() >= deadline) return;
119
+ revalidatePlugin();
120
+ const current = source.read(id);
121
+ if (
122
+ current.goal.revision < snapshot.goal.revision ||
123
+ current.goal.controlGeneration < snapshot.goal.controlGeneration
124
+ )
125
+ fail("CLI_GOAL_EVENT_SOURCE_REGRESSED");
126
+ if (current.digest === snapshot.digest) continue;
127
+ const previousStatus = snapshot.goal.status;
128
+ if (current.goal.status === previousStatus) {
129
+ snapshot = current;
130
+ continue;
131
+ }
132
+ const goalRef = reference(current.goal);
133
+ // occurredAt is observation time: polling cannot establish mutation time.
134
+ const observedAt = new Date().toISOString();
135
+ const event = await _deps.projectGoalExtensionEvent(
136
+ ref,
137
+ { type: TYPE, goalRef },
138
+ {
139
+ ...admission,
140
+ authorize(context) {
141
+ if (
142
+ context.operation !== "read-event" ||
143
+ context.type !== TYPE ||
144
+ JSON.stringify(context.goalRef) !== JSON.stringify(goalRef)
145
+ )
146
+ return false;
147
+ source.revalidate(current);
148
+ revalidatePlugin();
149
+ return true;
150
+ },
151
+ readEvent() {
152
+ source.revalidate(current);
153
+ return {
154
+ summary: `status: ${previousStatus} -> ${current.goal.status}`,
155
+ occurredAt: observedAt,
156
+ };
157
+ },
158
+ },
159
+ );
160
+ source.revalidate(current);
161
+ revalidatePlugin();
162
+ if (signal?.aborted || performance.now() >= deadline) return;
163
+ snapshot = current;
164
+ yield Object.freeze({
165
+ schema: CLI_GOAL_EVENT_SCHEMA,
166
+ kind: "event",
167
+ plugin: ref,
168
+ observedAt,
169
+ delivery: "polling",
170
+ replay: false,
171
+ event,
172
+ });
173
+ }
174
+ })();
175
+ }
@@ -1,7 +1,7 @@
1
1
  /** Explicit goal panels from standalone files or a short-lived desktop read lease. */
2
2
  import path from "node:path";
3
3
  import { createHash } from "node:crypto";
4
- import { VERSION } from "../../constants.js";
4
+ import { cliGoalExtensionHost } from "./cli-goal-capabilities.js";
5
5
  import { resolveConfigDataRoot } from "../paths.js";
6
6
  import { managedSettingsPath } from "../settings-loader.cjs";
7
7
  import { createCliGoalSnapshotReader } from "../cli-goal-source.js";
@@ -13,11 +13,9 @@ import {
13
13
  } from "./goal-extensions.js";
14
14
 
15
15
  export const CLI_GOAL_PANEL_SCHEMA = "chainlesschain.goal-extension-panels.v1";
16
- export const CLI_GOAL_EXTENSION_HOST = Object.freeze({
17
- id: "cli",
18
- version: VERSION,
19
- capabilities: Object.freeze(["panel:goal.summary"]),
20
- });
16
+ export const CLI_GOAL_EXTENSION_HOST = cliGoalExtensionHost([
17
+ "panel:goal.summary",
18
+ ]);
21
19
  export const _deps = { collectGoalExtensions, projectGoalExtensionPanel };
22
20
 
23
21
  function fail(code) {
@@ -95,13 +93,7 @@ export async function readCliGoalExtensionPanels(id, options = {}) {
95
93
  env,
96
94
  managedSettingsFile,
97
95
  host: connection?.evidence
98
- ? Object.freeze({
99
- ...CLI_GOAL_EXTENSION_HOST,
100
- capabilities: Object.freeze([
101
- "panel:goal.summary",
102
- "panel:goal.evidence",
103
- ]),
104
- })
96
+ ? cliGoalExtensionHost(["panel:goal.summary", "panel:goal.evidence"])
105
97
  : CLI_GOAL_EXTENSION_HOST,
106
98
  async authorize(context) {
107
99
  if (
@@ -169,6 +161,22 @@ export async function readCliGoalExtensionPanels(id, options = {}) {
169
161
  const projected = [];
170
162
  try {
171
163
  for (const panel of plugin.declaration.panels) {
164
+ if (
165
+ !hostOptions.host.activeCapabilities.includes(`panel:${panel.slot}`)
166
+ ) {
167
+ if (
168
+ !dropped.some(
169
+ (entry) =>
170
+ entry.name === plugin.name && entry.scope === plugin.scope,
171
+ )
172
+ )
173
+ dropped.push({
174
+ name: plugin.name,
175
+ scope: plugin.scope,
176
+ reason: "unsupported_capabilities",
177
+ });
178
+ continue;
179
+ }
172
180
  projected.push({
173
181
  plugin: ref,
174
182
  ...(await _deps.projectGoalExtensionPanel(