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.
- package/README.md +161 -13
- package/package.json +2 -2
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/goal.js +161 -0
- package/src/data/changelog.json +26 -0
- package/src/lib/evolution/evolution-artifact-ports.js +64 -1
- package/src/lib/plugin-runtime/cli-goal-capabilities.js +28 -0
- package/src/lib/plugin-runtime/cli-goal-draft-request.js +2 -6
- package/src/lib/plugin-runtime/cli-goal-drafts.js +2 -6
- package/src/lib/plugin-runtime/cli-goal-event-history.js +284 -0
- package/src/lib/plugin-runtime/cli-goal-events.js +175 -0
- package/src/lib/plugin-runtime/cli-goal-extension-host.js +21 -13
- package/src/lib/plugin-runtime/cli-project-goal-events.js +212 -0
- package/src/lib/plugin-runtime/examples/goal-project-observer/plugin.json +20 -0
- package/src/lib/plugin-runtime/examples/goal-status-observer/plugin.json +17 -0
- package/src/lib/plugin-runtime/examples/project-delivery-assistant/README.md +92 -0
- package/src/lib/plugin-runtime/examples/project-delivery-assistant/plugin.json +6 -7
- package/src/lib/plugin-runtime/goal-extensions.js +16 -0
- package/src/lib/project-goal-connection.js +111 -27
- package/src/lib/read-file-loop-guard.js +46 -11
- package/src/lib/tool-result-preview.js +1 -0
- package/src/runtime/agent-core.js +58 -0
|
@@ -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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
?
|
|
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(
|