instar 1.2.5 → 1.2.7
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/dist/core/SafeFsExecutor.d.ts.map +1 -1
- package/dist/core/SafeFsExecutor.js +35 -0
- package/dist/core/SafeFsExecutor.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts +68 -0
- package/dist/server/CapabilityIndex.d.ts.map +1 -0
- package/dist/server/CapabilityIndex.js +711 -0
- package/dist/server/CapabilityIndex.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +11 -400
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/1.2.6.md +42 -0
- package/upgrades/1.2.7.md +82 -0
- package/upgrades/side-effects/capabilities-introspection.md +119 -0
- package/upgrades/side-effects/safe-fs-agent-state-carveout.md +98 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CapabilityIndex — single source of truth for the /capabilities self-discovery
|
|
3
|
+
* surface AND the discoverability lint that PR #290 introduced.
|
|
4
|
+
*
|
|
5
|
+
* Before this module: /capabilities was a hand-curated 440-line object literal
|
|
6
|
+
* in routes.ts, and the lint test in tests/unit/capabilities-discoverability.test.ts
|
|
7
|
+
* kept a parallel hand-written INTERNAL_ALLOWLIST. Two manual surfaces had to
|
|
8
|
+
* stay in sync for the discoverability promise to hold; nothing structurally
|
|
9
|
+
* enforced that sync. Echo's case study (topic 11141, 2026-05-21) called out
|
|
10
|
+
* the third manual edit point as the next obvious risk.
|
|
11
|
+
*
|
|
12
|
+
* After this module: /capabilities iterates CAPABILITY_INDEX and the lint
|
|
13
|
+
* reads the same array. Adding a new top-level route prefix to routes.ts
|
|
14
|
+
* fails CI until the author either adds an entry to CAPABILITY_INDEX (the
|
|
15
|
+
* "this is agent-facing, surface it" choice) or adds the prefix to
|
|
16
|
+
* INTERNAL_PREFIXES (the "this is operator-only, skip discovery" choice).
|
|
17
|
+
* The lint test no longer carries policy in test code — it just enforces the
|
|
18
|
+
* invariant that every prefix has been classified.
|
|
19
|
+
*
|
|
20
|
+
* Each entry has a `build(input)` function that produces the block to merge
|
|
21
|
+
* into the /capabilities response. Builders are pure (no I/O beyond what
|
|
22
|
+
* ctx already exposes) so /capabilities stays cheap to call on every probe.
|
|
23
|
+
*
|
|
24
|
+
* Spec: docs/specs/capabilities-introspection.md (PR #N — follow-up #2 of two).
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { SafeGitExecutor } from '../core/SafeGitExecutor.js';
|
|
29
|
+
// ── Top-level capability entries ─────────────────────────────────────────
|
|
30
|
+
export const CAPABILITY_INDEX = [
|
|
31
|
+
{
|
|
32
|
+
key: 'telegram',
|
|
33
|
+
prefixes: ['/telegram'],
|
|
34
|
+
description: 'Telegram messaging adapter (bidirectional)',
|
|
35
|
+
build: ({ ctx, scripts }) => {
|
|
36
|
+
const hasTelegramConfig = ctx.config.messaging.some((m) => m.type === 'telegram' && m.enabled);
|
|
37
|
+
const hasTelegramReplyScript = scripts.includes('telegram-reply.sh');
|
|
38
|
+
return {
|
|
39
|
+
configured: hasTelegramConfig,
|
|
40
|
+
replyScript: hasTelegramReplyScript,
|
|
41
|
+
adapter: !!ctx.telegram,
|
|
42
|
+
bidirectional: hasTelegramConfig && hasTelegramReplyScript && !!ctx.telegram,
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'imessage',
|
|
48
|
+
prefixes: ['/imessage'],
|
|
49
|
+
description: 'iMessage adapter + AppleScript send pipeline',
|
|
50
|
+
build: ({ ctx }) => {
|
|
51
|
+
const hasIMessageConfig = ctx.config.messaging?.some((m) => m.type === 'imessage' && m.enabled) ??
|
|
52
|
+
false;
|
|
53
|
+
return {
|
|
54
|
+
configured: hasIMessageConfig,
|
|
55
|
+
adapter: !!ctx.imessage,
|
|
56
|
+
connected: ctx.imessage?.getConnectionInfo().state === 'connected',
|
|
57
|
+
endpoints: ctx.imessage
|
|
58
|
+
? [
|
|
59
|
+
'GET /imessage/status',
|
|
60
|
+
'POST /imessage/validate-send/:recipient — validate + get send token (Layer 3)',
|
|
61
|
+
'POST /imessage/reply/:recipient — confirm delivery with token (called by imessage-reply.sh)',
|
|
62
|
+
'GET /imessage/chats',
|
|
63
|
+
'GET /imessage/chats/:chatId/history',
|
|
64
|
+
'GET /imessage/search?q=...',
|
|
65
|
+
'GET /imessage/log-stats',
|
|
66
|
+
]
|
|
67
|
+
: [],
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'scheduler',
|
|
73
|
+
prefixes: ['/jobs'],
|
|
74
|
+
description: 'Cron-based job scheduler',
|
|
75
|
+
build: ({ ctx }) => {
|
|
76
|
+
let jobCount = 0;
|
|
77
|
+
let jobSlugs = [];
|
|
78
|
+
if (ctx.scheduler) {
|
|
79
|
+
const jobs = ctx.scheduler.getJobs();
|
|
80
|
+
jobCount = jobs.length;
|
|
81
|
+
jobSlugs = jobs.map((j) => j.slug);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
enabled: ctx.config.scheduler.enabled,
|
|
85
|
+
jobCount,
|
|
86
|
+
jobSlugs,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: 'relationships',
|
|
92
|
+
prefixes: ['/relationships'],
|
|
93
|
+
description: 'Relationship registry — context for personalized interactions',
|
|
94
|
+
build: ({ ctx }) => {
|
|
95
|
+
const relationshipsDir = ctx.config.relationships?.relationshipsDir;
|
|
96
|
+
let relationshipCount = 0;
|
|
97
|
+
if (relationshipsDir && fs.existsSync(relationshipsDir)) {
|
|
98
|
+
try {
|
|
99
|
+
relationshipCount = fs
|
|
100
|
+
.readdirSync(relationshipsDir)
|
|
101
|
+
.filter((f) => f.endsWith('.json')).length;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* ignore */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
enabled: !!ctx.config.relationships,
|
|
109
|
+
count: relationshipCount,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: 'feedback',
|
|
115
|
+
prefixes: ['/feedback'],
|
|
116
|
+
description: 'Feedback channel to upstream maintainers',
|
|
117
|
+
build: ({ ctx }) => ({ enabled: !!ctx.config.feedback?.enabled }),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: 'publishing',
|
|
121
|
+
prefixes: ['/publish', '/published'],
|
|
122
|
+
description: 'Public web pages via Telegraph (PUBLIC content)',
|
|
123
|
+
build: ({ ctx }) => ({
|
|
124
|
+
enabled: !!ctx.publisher,
|
|
125
|
+
pageCount: ctx.publisher?.listPages().length ?? 0,
|
|
126
|
+
warning: 'Telegraph pages are PUBLIC — anyone with the URL can view them.',
|
|
127
|
+
endpoints: ctx.publisher
|
|
128
|
+
? [
|
|
129
|
+
'POST /publish — publish markdown as a public Telegraph page',
|
|
130
|
+
'GET /published — list all published pages',
|
|
131
|
+
'PUT /publish/:pagePath — edit a published page',
|
|
132
|
+
]
|
|
133
|
+
: [],
|
|
134
|
+
}),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: 'privateViewer',
|
|
138
|
+
prefixes: ['/view', '/views'],
|
|
139
|
+
description: 'Private auth-gated HTML views (the safe sharing surface)',
|
|
140
|
+
build: ({ ctx }) => ({
|
|
141
|
+
enabled: !!ctx.viewer,
|
|
142
|
+
viewCount: ctx.viewer?.list().length ?? 0,
|
|
143
|
+
endpoints: ctx.viewer
|
|
144
|
+
? [
|
|
145
|
+
'POST /view — create a private auth-gated HTML view',
|
|
146
|
+
'GET /views — list all private views',
|
|
147
|
+
'GET /view/:viewId — render a private view (HTML)',
|
|
148
|
+
'PUT /view/:viewId — update a view',
|
|
149
|
+
'DELETE /view/:viewId — delete a view',
|
|
150
|
+
]
|
|
151
|
+
: [],
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
key: 'tunnel',
|
|
156
|
+
prefixes: ['/tunnel'],
|
|
157
|
+
description: 'Cloudflare tunnel for remote access to private viewer + API',
|
|
158
|
+
build: ({ ctx }) => ({
|
|
159
|
+
enabled: !!ctx.tunnel,
|
|
160
|
+
running: ctx.tunnel?.isRunning ?? false,
|
|
161
|
+
url: ctx.tunnel?.url ?? null,
|
|
162
|
+
type: ctx.config.tunnel?.type ?? null,
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
key: 'users',
|
|
167
|
+
prefixes: [],
|
|
168
|
+
description: 'Multi-user registry — counts only; no prefix because user routes live under other prefixes',
|
|
169
|
+
build: ({ ctx }) => {
|
|
170
|
+
let userCount = 0;
|
|
171
|
+
const usersFile = path.join(ctx.config.stateDir, 'users.json');
|
|
172
|
+
if (fs.existsSync(usersFile)) {
|
|
173
|
+
try {
|
|
174
|
+
const users = JSON.parse(fs.readFileSync(usersFile, 'utf-8'));
|
|
175
|
+
if (Array.isArray(users))
|
|
176
|
+
userCount = users.length;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
/* ignore */
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { count: userCount };
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
key: 'secrets',
|
|
187
|
+
prefixes: ['/secrets'],
|
|
188
|
+
description: 'Secret Drop — secure one-time-link credential intake from users',
|
|
189
|
+
build: ({ secretDrop }) => ({
|
|
190
|
+
// Secret Drop — surfaced explicitly because /capabilities is the
|
|
191
|
+
// documented self-discovery primitive. Agents that don't see this
|
|
192
|
+
// block here will reach for unsafe channels (chat paste, env vars).
|
|
193
|
+
enabled: true,
|
|
194
|
+
pending: secretDrop.listPending().length,
|
|
195
|
+
endpoints: [
|
|
196
|
+
'POST /secrets/request — create a one-time submission link',
|
|
197
|
+
'GET /secrets/pending — list pending submissions',
|
|
198
|
+
'POST /secrets/retrieve/:token — retrieve a submitted secret (one-time read)',
|
|
199
|
+
'DELETE /secrets/pending/:token — cancel a pending request',
|
|
200
|
+
],
|
|
201
|
+
retrievalHint: 'ALWAYS retrieve via `node .instar/scripts/secret-drop-retrieve.mjs TOKEN field-name` (use `--names` to discover fields). It streams the value to stdout and NEVER prints the response body. Raw curl against /secrets/retrieve dumps the secret into the Bash tool transcript — do not use it.',
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
key: 'topicMemory',
|
|
206
|
+
prefixes: ['/topic'],
|
|
207
|
+
description: 'Conversation memory keyed by Telegram topic',
|
|
208
|
+
build: ({ ctx }) => ({
|
|
209
|
+
enabled: !!ctx.topicMemory,
|
|
210
|
+
stats: ctx.topicMemory?.stats() ?? null,
|
|
211
|
+
endpoints: ctx.topicMemory
|
|
212
|
+
? [
|
|
213
|
+
'GET /topic/search?q=...&topic=N&limit=20',
|
|
214
|
+
'GET /topic/context/:topicId?recent=30',
|
|
215
|
+
'GET /topic/context/:topicId?assembled=true&prompt=...',
|
|
216
|
+
'GET /session/context/:topicId?prompt=...&job=...',
|
|
217
|
+
'GET /topic/list',
|
|
218
|
+
'GET /topic/stats',
|
|
219
|
+
'POST /topic/summarize { topicId }',
|
|
220
|
+
'POST /topic/summary { topicId, summary, messageCount, lastMessageId }',
|
|
221
|
+
]
|
|
222
|
+
: [],
|
|
223
|
+
}),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: 'monitoring',
|
|
227
|
+
prefixes: ['/monitoring'],
|
|
228
|
+
description: 'Runtime monitoring — quotas, watchdogs, degradations, etc.',
|
|
229
|
+
build: ({ ctx }) => ctx.config.monitoring,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
key: 'evolution',
|
|
233
|
+
prefixes: ['/evolution'],
|
|
234
|
+
description: 'Evolution proposals + learnings + gaps + actions',
|
|
235
|
+
build: ({ ctx }) => ({
|
|
236
|
+
enabled: !!ctx.evolution,
|
|
237
|
+
subsystems: ['proposals', 'learnings', 'gaps', 'actions'],
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
key: 'autonomy',
|
|
242
|
+
prefixes: ['/autonomy'],
|
|
243
|
+
description: 'Autonomy profile — how much freedom the agent has',
|
|
244
|
+
build: ({ ctx }) => ctx.autonomyManager
|
|
245
|
+
? {
|
|
246
|
+
enabled: true,
|
|
247
|
+
profile: ctx.autonomyManager.getProfile(),
|
|
248
|
+
endpoints: [
|
|
249
|
+
'GET /autonomy — full dashboard with profile, resolved state, summary, elevations',
|
|
250
|
+
'GET /autonomy/summary — natural language summary',
|
|
251
|
+
'POST /autonomy/profile { profile, reason } — set profile level',
|
|
252
|
+
'PATCH /autonomy/notifications — update notification preferences',
|
|
253
|
+
'GET /autonomy/history — profile change history',
|
|
254
|
+
],
|
|
255
|
+
}
|
|
256
|
+
: { enabled: false },
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
key: 'git',
|
|
260
|
+
prefixes: ['/git'],
|
|
261
|
+
description: 'Git state — repo, remote, sync job status + hints',
|
|
262
|
+
build: ({ ctx }) => {
|
|
263
|
+
const projectDir = ctx.config.projectDir;
|
|
264
|
+
const hasGitRepo = fs.existsSync(path.join(projectDir, '.git'));
|
|
265
|
+
let hasRemote = false;
|
|
266
|
+
let gitSyncJobEnabled = false;
|
|
267
|
+
if (hasGitRepo) {
|
|
268
|
+
try {
|
|
269
|
+
const remote = SafeGitExecutor.readSync(['remote'], {
|
|
270
|
+
cwd: projectDir,
|
|
271
|
+
stdio: 'pipe',
|
|
272
|
+
operation: 'src/server/CapabilityIndex.ts:git.build',
|
|
273
|
+
})
|
|
274
|
+
.toString()
|
|
275
|
+
.trim();
|
|
276
|
+
hasRemote = remote.length > 0;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
/* no remote */
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (ctx.scheduler) {
|
|
283
|
+
const gitJob = ctx.scheduler
|
|
284
|
+
.getJobs()
|
|
285
|
+
.find((j) => j.slug === 'git-sync');
|
|
286
|
+
gitSyncJobEnabled = !!gitJob?.enabled;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
inRepo: hasGitRepo,
|
|
290
|
+
hasRemote,
|
|
291
|
+
gitSyncJob: gitSyncJobEnabled,
|
|
292
|
+
autoSyncing: hasGitRepo && hasRemote && gitSyncJobEnabled,
|
|
293
|
+
agentType: ctx.config.agentType || 'standalone',
|
|
294
|
+
hint: hasGitRepo && hasRemote && gitSyncJobEnabled
|
|
295
|
+
? 'Git sync is active — your state is automatically committed and pushed hourly.'
|
|
296
|
+
: hasGitRepo && hasRemote
|
|
297
|
+
? 'Git repo with remote exists but git-sync job is not enabled. Enable it in jobs.json.'
|
|
298
|
+
: hasGitRepo
|
|
299
|
+
? 'Git repo exists but no remote configured. Add one with `git remote add origin <url>`.'
|
|
300
|
+
: 'No git repo. For standalone agents, run `instar git init`. For project-bound, initialize the parent repo.',
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
key: 'dispatches',
|
|
306
|
+
prefixes: ['/dispatches'],
|
|
307
|
+
description: 'Inter-machine dispatch queue (pull/apply tasks)',
|
|
308
|
+
build: ({ ctx }) => ({
|
|
309
|
+
enabled: !!ctx.config.dispatches?.enabled,
|
|
310
|
+
autoDispatch: !!ctx.autoDispatcher,
|
|
311
|
+
}),
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
key: 'updates',
|
|
315
|
+
prefixes: ['/updates'],
|
|
316
|
+
description: 'Auto-update channel (npm pull + migrator)',
|
|
317
|
+
build: ({ ctx }) => ({ autoUpdate: !!ctx.autoUpdater }),
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
key: 'playbook',
|
|
321
|
+
prefixes: [],
|
|
322
|
+
description: 'Playbook context manifest — CLI-driven, no HTTP prefix surface',
|
|
323
|
+
build: ({ ctx }) => {
|
|
324
|
+
const playbookDir = path.join(ctx.config.stateDir, 'playbook');
|
|
325
|
+
const initialized = fs.existsSync(playbookDir);
|
|
326
|
+
let itemCount = 0;
|
|
327
|
+
let hasManifest = false;
|
|
328
|
+
if (initialized) {
|
|
329
|
+
const manifestPath = path.join(playbookDir, 'context-manifest.json');
|
|
330
|
+
hasManifest = fs.existsSync(manifestPath);
|
|
331
|
+
if (hasManifest) {
|
|
332
|
+
try {
|
|
333
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
334
|
+
itemCount = Array.isArray(manifest.items) ? manifest.items.length : 0;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
/* corrupt manifest */
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
initialized,
|
|
343
|
+
itemCount,
|
|
344
|
+
hasManifest,
|
|
345
|
+
commands: [
|
|
346
|
+
'instar playbook init — initialize the playbook system',
|
|
347
|
+
'instar playbook doctor — health check',
|
|
348
|
+
'instar playbook status — manifest overview',
|
|
349
|
+
'instar playbook list — all context items',
|
|
350
|
+
'instar playbook add — add a context item',
|
|
351
|
+
'instar playbook assemble — preview trigger-based assembly',
|
|
352
|
+
'instar playbook evaluate — run lifecycle (score, decay, dedup)',
|
|
353
|
+
'instar playbook mount — import context from another agent',
|
|
354
|
+
'instar playbook search — find items by tag',
|
|
355
|
+
],
|
|
356
|
+
hint: initialized
|
|
357
|
+
? `Playbook active with ${itemCount} context items. Run 'instar playbook doctor' to verify health.`
|
|
358
|
+
: "Playbook not initialized. Run 'instar playbook init' to set up adaptive context engineering.",
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
key: 'attentionQueue',
|
|
364
|
+
prefixes: ['/attention'],
|
|
365
|
+
description: 'User-attention queue (signal needs/reviews)',
|
|
366
|
+
build: () => ({
|
|
367
|
+
enabled: true,
|
|
368
|
+
hint: 'Use POST /attention to signal important items to the user.',
|
|
369
|
+
}),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
key: 'commitments',
|
|
373
|
+
prefixes: ['/commitments'],
|
|
374
|
+
description: 'CommitmentTracker — lifecycle for agent promises',
|
|
375
|
+
build: () => ({
|
|
376
|
+
enabled: true,
|
|
377
|
+
endpoints: [
|
|
378
|
+
'GET /commitments — list commitments (filterable)',
|
|
379
|
+
'GET /commitments/:id — fetch a single commitment',
|
|
380
|
+
'POST /commitments — open a new commitment',
|
|
381
|
+
'PATCH /commitments/:id — update status/details',
|
|
382
|
+
'POST /commitments/:id/deliver — mark delivered',
|
|
383
|
+
'POST /commitments/:id/withdraw — withdraw the commitment',
|
|
384
|
+
'POST /commitments/:id/resume — resume a paused commitment',
|
|
385
|
+
'GET /commitments/active-context — assemble active-commitment context',
|
|
386
|
+
],
|
|
387
|
+
}),
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
key: 'semantic',
|
|
391
|
+
prefixes: ['/semantic'],
|
|
392
|
+
description: 'Semantic memory — successor to deprecated /memory',
|
|
393
|
+
build: () => ({
|
|
394
|
+
enabled: true,
|
|
395
|
+
endpoints: [
|
|
396
|
+
'POST /semantic/remember — store a memory',
|
|
397
|
+
'GET /semantic/recall/:id — fetch a memory by id',
|
|
398
|
+
'GET /semantic/search?q=... — semantic search',
|
|
399
|
+
'GET /semantic/search/hybrid?q=... — hybrid (vector + keyword) search',
|
|
400
|
+
'POST /semantic/connect — link two memories',
|
|
401
|
+
'GET /semantic/explore/:id — graph traversal from a memory',
|
|
402
|
+
'POST /semantic/verify/:id — mark a memory verified',
|
|
403
|
+
'POST /semantic/supersede — replace one memory with another',
|
|
404
|
+
'GET /semantic/stale — list stale memories',
|
|
405
|
+
'GET /semantic/stats — store statistics',
|
|
406
|
+
'GET /semantic/context — assembled context for a prompt',
|
|
407
|
+
],
|
|
408
|
+
}),
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
key: 'tokens',
|
|
412
|
+
prefixes: ['/tokens'],
|
|
413
|
+
description: 'TokenLedger — read-only token-usage observability',
|
|
414
|
+
build: () => ({
|
|
415
|
+
enabled: true,
|
|
416
|
+
endpoints: [
|
|
417
|
+
'GET /tokens/summary — aggregate token usage',
|
|
418
|
+
'GET /tokens/sessions — per-session token usage',
|
|
419
|
+
'GET /tokens/by-project — usage grouped by project',
|
|
420
|
+
'GET /tokens/orphans — sessions with no project binding',
|
|
421
|
+
],
|
|
422
|
+
}),
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
key: 'skipLedger',
|
|
426
|
+
prefixes: ['/skip-ledger'],
|
|
427
|
+
description: 'Skip-ledger — workload-aware idempotency',
|
|
428
|
+
build: () => ({
|
|
429
|
+
enabled: true,
|
|
430
|
+
hint: 'Use GET /skip-ledger to avoid re-processing items in jobs.',
|
|
431
|
+
}),
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
key: 'projectMap',
|
|
435
|
+
prefixes: ['/project-map'],
|
|
436
|
+
description: 'Project map — file/structure self-awareness',
|
|
437
|
+
build: ({ ctx }) => ({
|
|
438
|
+
enabled: !!ctx.projectMapper,
|
|
439
|
+
hasSavedMap: ctx.projectMapper?.loadSavedMap() !== null,
|
|
440
|
+
endpoints: ctx.projectMapper
|
|
441
|
+
? [
|
|
442
|
+
'GET /project-map — full project structure (JSON, ?format=markdown, ?format=compact)',
|
|
443
|
+
'POST /project-map/refresh — regenerate the project map',
|
|
444
|
+
]
|
|
445
|
+
: [],
|
|
446
|
+
}),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
key: 'contextHierarchy',
|
|
450
|
+
prefixes: ['/context'],
|
|
451
|
+
description: 'Context hierarchy + dispatch table',
|
|
452
|
+
build: ({ ctx }) => ({
|
|
453
|
+
enabled: !!ctx.contextHierarchy,
|
|
454
|
+
segments: ctx.contextHierarchy
|
|
455
|
+
?.listSegments()
|
|
456
|
+
.map((s) => ({ id: s.id, tier: s.tier, exists: s.exists })) ?? [],
|
|
457
|
+
endpoints: ctx.contextHierarchy
|
|
458
|
+
? [
|
|
459
|
+
'GET /context — list all context segments with status',
|
|
460
|
+
'GET /context/dispatch — dispatch table (when X, load Y)',
|
|
461
|
+
'GET /context/:segmentId — load a specific context segment',
|
|
462
|
+
]
|
|
463
|
+
: [],
|
|
464
|
+
}),
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
key: 'workingMemory',
|
|
468
|
+
prefixes: [],
|
|
469
|
+
description: 'Working memory assembler — surfaced through /context/working-memory',
|
|
470
|
+
build: ({ ctx }) => ({
|
|
471
|
+
enabled: !!ctx.workingMemory,
|
|
472
|
+
endpoints: ctx.workingMemory
|
|
473
|
+
? [
|
|
474
|
+
'GET /context/working-memory?prompt=...&jobSlug=...&sessionId=... — token-budgeted context assembly from all memory layers',
|
|
475
|
+
]
|
|
476
|
+
: [],
|
|
477
|
+
}),
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
key: 'canonicalState',
|
|
481
|
+
prefixes: ['/state', '/projects'],
|
|
482
|
+
description: 'Canonical state — quick-facts, anti-patterns, projects',
|
|
483
|
+
build: ({ ctx }) => ({
|
|
484
|
+
enabled: !!ctx.canonicalState,
|
|
485
|
+
endpoints: ctx.canonicalState
|
|
486
|
+
? [
|
|
487
|
+
'GET /state/quick-facts — fast answers to common questions',
|
|
488
|
+
'POST /state/quick-facts — add/update a quick fact',
|
|
489
|
+
'GET /state/anti-patterns — things NOT to do',
|
|
490
|
+
'POST /state/anti-patterns — record a new anti-pattern',
|
|
491
|
+
'GET /state/projects — all known projects',
|
|
492
|
+
'POST /state/projects — register a project',
|
|
493
|
+
'GET /state/summary — compact state summary',
|
|
494
|
+
]
|
|
495
|
+
: [],
|
|
496
|
+
}),
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
key: 'coherence',
|
|
500
|
+
prefixes: ['/coherence', '/topic-bindings'],
|
|
501
|
+
description: 'Coherence gate — pre-action verification + topic bindings',
|
|
502
|
+
build: ({ ctx }) => ({
|
|
503
|
+
enabled: !!ctx.coherenceGate,
|
|
504
|
+
endpoints: ctx.coherenceGate
|
|
505
|
+
? [
|
|
506
|
+
'POST /coherence/check — pre-action coherence verification',
|
|
507
|
+
'POST /coherence/reflect — generate self-reflection prompt',
|
|
508
|
+
'GET /topic-bindings — list topic-project bindings',
|
|
509
|
+
'POST /topic-bindings — bind a topic to a project',
|
|
510
|
+
]
|
|
511
|
+
: [],
|
|
512
|
+
}),
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
key: 'responseReview',
|
|
516
|
+
prefixes: ['/review'],
|
|
517
|
+
description: 'Response-review pipeline + coherence proposals',
|
|
518
|
+
build: ({ ctx }) => ({
|
|
519
|
+
enabled: !!ctx.responseReviewGate,
|
|
520
|
+
endpoints: ctx.responseReviewGate
|
|
521
|
+
? [
|
|
522
|
+
'POST /review/evaluate — evaluate agent response before delivery',
|
|
523
|
+
'POST /review/test — dry-run test of review pipeline',
|
|
524
|
+
'GET /review/history — review audit log (filterable, 30-day retention)',
|
|
525
|
+
'DELETE /review/history?sessionId=X — delete history for session (DSAR)',
|
|
526
|
+
'GET /review/stats — reviewer effectiveness metrics (per-period, per-recipient)',
|
|
527
|
+
'GET /coherence/proposals — patch proposal queue',
|
|
528
|
+
'POST /coherence/proposals — submit a new proposal',
|
|
529
|
+
'POST /coherence/proposals/:id/approve — approve a proposal',
|
|
530
|
+
'POST /coherence/proposals/:id/reject — reject a proposal',
|
|
531
|
+
'GET /coherence/health — coherence evolution dashboard',
|
|
532
|
+
'GET /review/health — reviewer health and anomaly detection',
|
|
533
|
+
'POST /review/canary — run canary tests with known-bad messages',
|
|
534
|
+
]
|
|
535
|
+
: [],
|
|
536
|
+
}),
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
key: 'externalOperationSafety',
|
|
540
|
+
prefixes: ['/operations', '/sentinel', '/trust'],
|
|
541
|
+
description: 'External operation gate + message sentinel + adaptive-trust profile',
|
|
542
|
+
build: ({ ctx }) => ({
|
|
543
|
+
enabled: !!ctx.operationGate,
|
|
544
|
+
sentinel: !!ctx.sentinel,
|
|
545
|
+
adaptiveTrust: !!ctx.adaptiveTrust,
|
|
546
|
+
endpoints: ctx.operationGate
|
|
547
|
+
? [
|
|
548
|
+
'POST /operations/classify — classify an operation (risk level)',
|
|
549
|
+
'POST /operations/evaluate — full gate evaluation (proceed/plan/block)',
|
|
550
|
+
'GET /operations/log — recent operation history',
|
|
551
|
+
'GET /operations/permissions/:service — service permissions',
|
|
552
|
+
'POST /sentinel/classify — test message classification',
|
|
553
|
+
'GET /sentinel/stats — sentinel classification stats',
|
|
554
|
+
'GET /trust — full trust profile',
|
|
555
|
+
'GET /trust/summary — compact trust summary',
|
|
556
|
+
'POST /trust/grant — explicitly grant trust',
|
|
557
|
+
'GET /trust/elevations — pending elevation suggestions',
|
|
558
|
+
'GET /trust/changelog — recent trust changes',
|
|
559
|
+
]
|
|
560
|
+
: [],
|
|
561
|
+
}),
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
key: 'featureGuide',
|
|
565
|
+
prefixes: [],
|
|
566
|
+
description: 'Context-triggered capability suggestions (no own routes; meta-guidance)',
|
|
567
|
+
build: () => ({
|
|
568
|
+
description: 'Context-triggered capability suggestions. Use these proactively when context matches.',
|
|
569
|
+
triggers: FEATURE_GUIDE_TRIGGERS,
|
|
570
|
+
}),
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
key: 'discovery',
|
|
574
|
+
prefixes: ['/features'],
|
|
575
|
+
description: 'FeatureRegistry — opt-in feature discovery + per-user state',
|
|
576
|
+
build: ({ ctx }) => ctx.featureRegistry
|
|
577
|
+
? {
|
|
578
|
+
enabled: true,
|
|
579
|
+
featureCount: ctx.featureRegistry.getAllDefinitions().length,
|
|
580
|
+
summaries: ctx.featureRegistry.getSummaries(),
|
|
581
|
+
evaluator: ctx.discoveryEvaluator
|
|
582
|
+
? { active: true, ...ctx.discoveryEvaluator.getStatus() }
|
|
583
|
+
: { active: false },
|
|
584
|
+
endpoints: [
|
|
585
|
+
'GET /features — full feature registry (definitions + per-user state)',
|
|
586
|
+
'GET /features/:id — single feature details with valid transitions',
|
|
587
|
+
'GET /features?state=undiscovered,aware — filter by discovery state',
|
|
588
|
+
'GET /features/summary — lightweight summaries only',
|
|
589
|
+
'POST /features/evaluate-context — evaluate context for feature surfacing',
|
|
590
|
+
'GET /features/evaluator-status — evaluator rate-limit and cache status',
|
|
591
|
+
],
|
|
592
|
+
}
|
|
593
|
+
: { enabled: false },
|
|
594
|
+
},
|
|
595
|
+
];
|
|
596
|
+
// ── Feature-guide triggers (large static payload; isolated for readability) ──
|
|
597
|
+
const FEATURE_GUIDE_TRIGGERS = [
|
|
598
|
+
{ context: 'User mentions a document, file, or report', action: 'Render it as a private view (POST /view) — beautiful HTML accessible on any device. If tunnel is running, shareable remotely.' },
|
|
599
|
+
{ context: 'User wants to share something publicly', action: 'Publish via Telegraph (POST /publish). Always warn the user it is publicly accessible.' },
|
|
600
|
+
{ context: 'User mentions someone by name', action: 'Check relationships (GET /relationships). Use context to personalize interactions. Offer to start tracking if not found.' },
|
|
601
|
+
{ context: 'User has a recurring task', action: 'Create a scheduled job in .instar/jobs.json. Explain it will run automatically.' },
|
|
602
|
+
{ context: 'User repeats a workflow', action: 'Create a skill in .claude/skills/. It becomes a slash command for future sessions.' },
|
|
603
|
+
{ context: 'User is debugging CI or deployment', action: 'Check CI health (GET /ci) for GitHub Actions status.' },
|
|
604
|
+
{ context: 'User asks about past events or prior conversations', action: 'Search topic memory (GET /topic/search?q=...), get topic context (GET /topic/context/:topicId), check memory, review activity logs.' },
|
|
605
|
+
{ context: 'User frustrated with a limitation', action: 'Check for updates (GET /updates). Check dispatches (GET /dispatches/pending). The fix may already exist.' },
|
|
606
|
+
{ context: 'User asks to remember something', action: 'Write to .instar/MEMORY.md. Explain it persists across sessions.' },
|
|
607
|
+
{ context: 'Something needs user attention later', action: 'Queue in attention system (POST /attention). More reliable than hoping they see a message.' },
|
|
608
|
+
{ context: 'Job processes a list of items', action: 'Use skip ledger (POST /skip-ledger/workload) to avoid re-processing on next run.' },
|
|
609
|
+
{ context: 'About to deploy, push, or modify files outside project', action: 'Run coherence check FIRST (POST /coherence/check). Verify you are in the right project for the current topic.' },
|
|
610
|
+
{ context: 'Working on a topic tied to a specific project', action: 'Check topic-project binding (GET /topic-bindings). If unbound, bind it (POST /topic-bindings) to prevent cross-project confusion.' },
|
|
611
|
+
{ context: 'Unsure what project this is or what files exist', action: 'Check project map (GET /project-map?format=compact) for spatial awareness — project type, key files, deployment targets.' },
|
|
612
|
+
{ context: 'About to call an external service API (email, calendar, messaging)', action: 'Evaluate through operation gate FIRST (POST /operations/evaluate). The gate classifies risk and decides proceed/plan/block.' },
|
|
613
|
+
{ context: 'User says to stop, cancel, or abort', action: 'MessageSentinel intercepts these automatically. For manual classification: POST /sentinel/classify.' },
|
|
614
|
+
{ context: 'User says "you don\'t need to ask me about X"', action: 'Grant trust explicitly (POST /trust/grant). Trust persists across sessions.' },
|
|
615
|
+
{ context: 'User asks about autonomy, trust level, approval settings, or how much freedom the agent has', action: 'Show autonomy summary (GET /autonomy/summary). Present in natural language — never show CLI commands.' },
|
|
616
|
+
{ context: 'User says "go autonomous", "handle everything yourself", "I trust you", or wants less friction', action: 'Set autonomy profile (POST /autonomy/profile { profile: "autonomous", reason: "..." }). Confirm the implications conversationally first.' },
|
|
617
|
+
{ context: 'User says "supervise everything", "ask me first", "I want to approve", or wants more control', action: 'Set autonomy profile (POST /autonomy/profile { profile: "cautious" or "supervised", reason: "..." }).' },
|
|
618
|
+
{ context: 'User wants to undo an evolution change or revert agent self-modification', action: 'Check evolution proposals (GET /evolution/proposals?status=implemented), then PATCH the proposal status back. Explain what happened and what was reverted.' },
|
|
619
|
+
{ context: 'User asks to adjust memory warning thresholds or stop memory alerts', action: 'Update thresholds (PATCH /monitoring/memory/thresholds with {warning, elevated, critical}). Check current state (GET /monitoring/memory).' },
|
|
620
|
+
{ context: 'User asks about Instar features, architecture, multi-user, or multi-machine setup', action: 'STOP — do NOT answer from memory. Run GET /capabilities first, then check `instar --help` for CLI commands, then GET /context/dispatch for the full context map. Answer ONLY from what these return.' },
|
|
621
|
+
{ context: 'User says to update, install latest version, or apply updates', action: 'Run POST /updates/apply immediately. Do NOT explain how to update — just do it. If you want to enable auto-updates, set updates.autoApply to true in .instar/config.json.' },
|
|
622
|
+
{ context: 'Agent learns a lesson, discovers a pattern, or user teaches something worth remembering', action: 'Add it to the Playbook manifest (instar playbook add). Context items persist across sessions and get assembled based on triggers. This is more structured than MEMORY.md — items have triggers, scores, and lifecycle management.' },
|
|
623
|
+
{ context: 'Session context feels stale or irrelevant', action: 'Run instar playbook evaluate to score items, decay stale ones, and deduplicate. Then check instar playbook status.' },
|
|
624
|
+
{ context: 'Another agent wants to share context', action: 'Use instar playbook mount to import their manifest with integrity verification. Only global-scoped items are accepted.' },
|
|
625
|
+
{ context: 'User mentions other AI agents, wants to collaborate with other agents, or asks about multi-agent communication', action: 'Check if Threadline relay is enabled (GET /threadline/status). If not enabled, explain: you have the ability to connect to a secure agent network where you can discover and message other AI agents in real time. Offer to enable it conversationally. If enabled, use threadline_discover and threadline_send.' },
|
|
626
|
+
{ context: 'User asks what you can do, your features, or capabilities you have not used yet', action: 'Include Threadline relay in your capability overview if not yet enabled. Explain it lets you join a network of agents for real-time secure communication — available but waiting for their go-ahead.' },
|
|
627
|
+
];
|
|
628
|
+
// ── Internal-only prefixes (intentionally NOT surfaced in /capabilities) ──
|
|
629
|
+
/**
|
|
630
|
+
* Top-level route prefixes that are intentionally invisible to agents in
|
|
631
|
+
* the /capabilities response. Each entry is a deliberate "skip" with a
|
|
632
|
+
* one-line reason. The discoverability lint verifies that every prefix in
|
|
633
|
+
* routes.ts is either claimed by a CAPABILITY_INDEX entry OR allowlisted
|
|
634
|
+
* here — never both, never neither.
|
|
635
|
+
*/
|
|
636
|
+
export const INTERNAL_PREFIXES = [
|
|
637
|
+
{ prefix: 'health', reason: 'basic liveness check, no auth' },
|
|
638
|
+
{ prefix: 'ping', reason: 'synchronous noop, used by tunnel/lifeline probes' },
|
|
639
|
+
{ prefix: 'whoami', reason: 'internal identity probe (sentinel/relay layer 1c)' },
|
|
640
|
+
{ prefix: 'capabilities', reason: 'the discovery endpoint itself — surfacing would recurse' },
|
|
641
|
+
{ prefix: 'internal', reason: 'internal-only IPC namespace' },
|
|
642
|
+
{ prefix: 'pastes', reason: 'internal Claude Code paste-callback receiver' },
|
|
643
|
+
{ prefix: 'listener', reason: 'internal heartbeat/listener wiring' },
|
|
644
|
+
{ prefix: 'events', reason: 'internal SSE/event-stream' },
|
|
645
|
+
{ prefix: 'config', reason: 'global config CRUD used by setup/migrator' },
|
|
646
|
+
{ prefix: 'status', reason: 'legacy status endpoint, superseded by /capabilities' },
|
|
647
|
+
{ prefix: 'shared-state', reason: 'legacy state primitive, superseded by canonicalState' },
|
|
648
|
+
{ prefix: 'backups', reason: 'backup listing is operator-only, not agent-facing' },
|
|
649
|
+
{ prefix: 'episodes', reason: 'legacy episode log, replaced by topicMemory' },
|
|
650
|
+
{ prefix: 'reflection', reason: 'legacy reflection log, replaced by topicMemory' },
|
|
651
|
+
{ prefix: 'serendipity', reason: 'operator review surface, not agent-facing' },
|
|
652
|
+
{ prefix: 'system-review', reason: 'legacy system review log, replaced by responseReview' },
|
|
653
|
+
{ prefix: 'system-reviews', reason: 'legacy system review log, replaced by responseReview' },
|
|
654
|
+
{ prefix: 'systems', reason: 'legacy systems registry, replaced by canonicalState.projects' },
|
|
655
|
+
{ prefix: 'memory', reason: 'deprecated (Deprecation/Sunset headers) → /semantic' },
|
|
656
|
+
{ prefix: 'messaging', reason: 'alternative surface for /telegram, /imessage, etc.' },
|
|
657
|
+
{ prefix: 'messages', reason: 'legacy direct message access' },
|
|
658
|
+
{ prefix: 'providers', reason: 'legacy provider registry, replaced by autonomy' },
|
|
659
|
+
{ prefix: 'quota', reason: 'operator-only quota observability' },
|
|
660
|
+
{ prefix: 'watchdog', reason: 'operator-only watchdog state' },
|
|
661
|
+
{ prefix: 'telemetry', reason: 'operator-only telemetry plumbing' },
|
|
662
|
+
{ prefix: 'homeostasis', reason: 'operator-only homeostasis state' },
|
|
663
|
+
{ prefix: 'agents', reason: 'surfaced via threadline discovery' },
|
|
664
|
+
{ prefix: 'delivery-queue', reason: 'operator-only relay queue observability' },
|
|
665
|
+
{ prefix: 'prompt-gate', reason: 'operator-only prompt-gate observability' },
|
|
666
|
+
{ prefix: 'scope-coherence', reason: 'operator-only scope-coherence observability' },
|
|
667
|
+
{ prefix: 'slack', reason: 'surfaced via messaging adapters' },
|
|
668
|
+
{ prefix: 'whatsapp', reason: 'surfaced via messaging adapters' },
|
|
669
|
+
{ prefix: 'flows', reason: 'surfaced inside `evolution` subsystems' },
|
|
670
|
+
{ prefix: 'initiatives', reason: 'surfaced inside `evolution` subsystems' },
|
|
671
|
+
{ prefix: 'triage', reason: 'surfaced inside `evolution` subsystems' },
|
|
672
|
+
{ prefix: 'intent', reason: 'surfaced inside `evolution` subsystems' },
|
|
673
|
+
{ prefix: 'self-knowledge', reason: 'surfaced inside `capability-map`' },
|
|
674
|
+
{ prefix: 'capability-map', reason: 'separate self-knowledge surface with its own discovery path' },
|
|
675
|
+
{ prefix: 'build', reason: 'operator-only build endpoint' },
|
|
676
|
+
{ prefix: 'sessions', reason: 'operator/dashboard-only session listing (no agent-facing API)' },
|
|
677
|
+
{ prefix: 'ci', reason: 'operator-only CI status surface' },
|
|
678
|
+
{ prefix: 'session', reason: 'single-session context surfaced via topicMemory endpoints' },
|
|
679
|
+
{ prefix: 'identity', reason: 'identity files surfaced via the top-level `identity` field of the response' },
|
|
680
|
+
{ prefix: 'hooks', reason: 'hook listing surfaced via the top-level `hooks` field of the response' },
|
|
681
|
+
{ prefix: 'threadline', reason: 'surfaced via discovery (threadline-relay feature)' },
|
|
682
|
+
];
|
|
683
|
+
// ── Public helpers ───────────────────────────────────────────────────────
|
|
684
|
+
/**
|
|
685
|
+
* Map every top-level route prefix to its CapabilityIndex key (for entries
|
|
686
|
+
* that claim prefixes). Lint uses this to verify routes.ts has full coverage.
|
|
687
|
+
*/
|
|
688
|
+
export function buildPrefixToKeyMap() {
|
|
689
|
+
const m = new Map();
|
|
690
|
+
for (const entry of CAPABILITY_INDEX) {
|
|
691
|
+
for (const prefix of entry.prefixes) {
|
|
692
|
+
// Strip leading slash for symmetry with route extraction.
|
|
693
|
+
const normalized = prefix.startsWith('/') ? prefix.slice(1) : prefix;
|
|
694
|
+
m.set(normalized, entry.key);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return m;
|
|
698
|
+
}
|
|
699
|
+
/** Set of all prefixes that should NOT appear in /capabilities. */
|
|
700
|
+
export function buildInternalPrefixSet() {
|
|
701
|
+
return new Set(INTERNAL_PREFIXES.map((e) => e.prefix));
|
|
702
|
+
}
|
|
703
|
+
/** Iterate the index and produce the full capability-block object. */
|
|
704
|
+
export function buildAllCapabilityBlocks(input) {
|
|
705
|
+
const blocks = {};
|
|
706
|
+
for (const entry of CAPABILITY_INDEX) {
|
|
707
|
+
blocks[entry.key] = entry.build(input);
|
|
708
|
+
}
|
|
709
|
+
return blocks;
|
|
710
|
+
}
|
|
711
|
+
//# sourceMappingURL=CapabilityIndex.js.map
|