@soleri/core 9.4.0 → 9.6.0
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/adapters/claude-code-adapter.d.ts +27 -0
- package/dist/adapters/claude-code-adapter.d.ts.map +1 -0
- package/dist/adapters/claude-code-adapter.js +111 -0
- package/dist/adapters/claude-code-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/registry.d.ts +21 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +44 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/types.d.ts +93 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +10 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/brain/brain.d.ts +12 -1
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +106 -44
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +36 -30
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/chat/agent-loop.js +1 -1
- package/dist/chat/agent-loop.js.map +1 -1
- package/dist/chat/notifications.d.ts.map +1 -1
- package/dist/chat/notifications.js +4 -0
- package/dist/chat/notifications.js.map +1 -1
- package/dist/control/intent-router.d.ts +1 -0
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +11 -5
- package/dist/control/intent-router.js.map +1 -1
- package/dist/curator/curator.d.ts +4 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +141 -27
- package/dist/curator/curator.js.map +1 -1
- package/dist/hooks/candidate-scorer.d.ts +28 -0
- package/dist/hooks/candidate-scorer.d.ts.map +1 -0
- package/dist/hooks/candidate-scorer.js +20 -0
- package/dist/hooks/candidate-scorer.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +1 -0
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/packs/index.d.ts +3 -2
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +3 -2
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/lockfile.d.ts +23 -1
- package/dist/packs/lockfile.d.ts.map +1 -1
- package/dist/packs/lockfile.js +50 -4
- package/dist/packs/lockfile.js.map +1 -1
- package/dist/packs/pack-installer.d.ts +10 -0
- package/dist/packs/pack-installer.d.ts.map +1 -1
- package/dist/packs/pack-installer.js +69 -2
- package/dist/packs/pack-installer.js.map +1 -1
- package/dist/packs/pack-lifecycle.d.ts +50 -0
- package/dist/packs/pack-lifecycle.d.ts.map +1 -0
- package/dist/packs/pack-lifecycle.js +91 -0
- package/dist/packs/pack-lifecycle.js.map +1 -0
- package/dist/packs/types.d.ts +64 -44
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +9 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/persistence/sqlite-provider.d.ts +5 -1
- package/dist/persistence/sqlite-provider.d.ts.map +1 -1
- package/dist/persistence/sqlite-provider.js +22 -2
- package/dist/persistence/sqlite-provider.js.map +1 -1
- package/dist/planning/github-projection.d.ts +8 -8
- package/dist/planning/github-projection.d.ts.map +1 -1
- package/dist/planning/github-projection.js +42 -42
- package/dist/planning/github-projection.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +6 -1
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/plugins/types.d.ts +21 -21
- package/dist/queue/pipeline-runner.d.ts.map +1 -1
- package/dist/queue/pipeline-runner.js +4 -0
- package/dist/queue/pipeline-runner.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +9 -1
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +169 -0
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +133 -4
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +128 -90
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +44 -11
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/shutdown-registry.d.ts +36 -0
- package/dist/runtime/shutdown-registry.d.ts.map +1 -0
- package/dist/runtime/shutdown-registry.js +74 -0
- package/dist/runtime/shutdown-registry.js.map +1 -0
- package/dist/runtime/types.d.ts +10 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/subagent/concurrency-manager.d.ts +29 -0
- package/dist/subagent/concurrency-manager.d.ts.map +1 -0
- package/dist/subagent/concurrency-manager.js +73 -0
- package/dist/subagent/concurrency-manager.js.map +1 -0
- package/dist/subagent/dispatcher.d.ts +41 -0
- package/dist/subagent/dispatcher.d.ts.map +1 -0
- package/dist/subagent/dispatcher.js +259 -0
- package/dist/subagent/dispatcher.js.map +1 -0
- package/dist/subagent/index.d.ts +14 -0
- package/dist/subagent/index.d.ts.map +1 -0
- package/dist/subagent/index.js +15 -0
- package/dist/subagent/index.js.map +1 -0
- package/dist/subagent/orphan-reaper.d.ts +37 -0
- package/dist/subagent/orphan-reaper.d.ts.map +1 -0
- package/dist/subagent/orphan-reaper.js +71 -0
- package/dist/subagent/orphan-reaper.js.map +1 -0
- package/dist/subagent/result-aggregator.d.ts +7 -0
- package/dist/subagent/result-aggregator.d.ts.map +1 -0
- package/dist/subagent/result-aggregator.js +57 -0
- package/dist/subagent/result-aggregator.js.map +1 -0
- package/dist/subagent/task-checkout.d.ts +36 -0
- package/dist/subagent/task-checkout.d.ts.map +1 -0
- package/dist/subagent/task-checkout.js +52 -0
- package/dist/subagent/task-checkout.js.map +1 -0
- package/dist/subagent/types.d.ts +114 -0
- package/dist/subagent/types.d.ts.map +1 -0
- package/dist/subagent/types.js +9 -0
- package/dist/subagent/types.js.map +1 -0
- package/dist/subagent/workspace-resolver.d.ts +35 -0
- package/dist/subagent/workspace-resolver.d.ts.map +1 -0
- package/dist/subagent/workspace-resolver.js +99 -0
- package/dist/subagent/workspace-resolver.js.map +1 -0
- package/dist/transport/http-server.d.ts.map +1 -1
- package/dist/transport/http-server.js +49 -3
- package/dist/transport/http-server.js.map +1 -1
- package/dist/transport/ws-server.d.ts.map +1 -1
- package/dist/transport/ws-server.js +7 -0
- package/dist/transport/ws-server.js.map +1 -1
- package/dist/vault/linking.d.ts +3 -4
- package/dist/vault/linking.d.ts.map +1 -1
- package/dist/vault/linking.js +79 -32
- package/dist/vault/linking.js.map +1 -1
- package/dist/vault/vault-maintenance.d.ts.map +1 -1
- package/dist/vault/vault-maintenance.js +7 -14
- package/dist/vault/vault-maintenance.js.map +1 -1
- package/dist/vault/vault-memories.d.ts.map +1 -1
- package/dist/vault/vault-memories.js +19 -9
- package/dist/vault/vault-memories.js.map +1 -1
- package/dist/vault/vault-schema.d.ts +1 -0
- package/dist/vault/vault-schema.d.ts.map +1 -1
- package/dist/vault/vault-schema.js +20 -0
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +7 -3
- package/dist/vault/vault.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/adapters/claude-code-adapter.test.ts +167 -0
- package/src/__tests__/adapters/registry.test.ts +100 -0
- package/src/__tests__/packs/pack-lifecycle.test.ts +379 -0
- package/src/__tests__/subagent/concurrency-manager.test.ts +132 -0
- package/src/__tests__/subagent/dispatcher.test.ts +195 -0
- package/src/__tests__/subagent/orphan-reaper.test.ts +141 -0
- package/src/__tests__/subagent/result-aggregator.test.ts +141 -0
- package/src/__tests__/subagent/task-checkout.test.ts +86 -0
- package/src/__tests__/subagent/workspace-resolver.test.ts +138 -0
- package/src/adapters/claude-code-adapter.ts +163 -0
- package/src/adapters/index.ts +22 -0
- package/src/adapters/registry.ts +53 -0
- package/src/adapters/types.ts +114 -0
- package/src/brain/brain.ts +120 -46
- package/src/brain/intelligence.ts +42 -34
- package/src/chat/agent-loop.ts +1 -1
- package/src/chat/notifications.ts +4 -0
- package/src/control/intent-router.ts +10 -8
- package/src/curator/curator.ts +146 -29
- package/src/hooks/candidate-scorer.test.ts +76 -0
- package/src/hooks/candidate-scorer.ts +39 -0
- package/src/index.ts +40 -1
- package/src/llm/llm-client.ts +1 -0
- package/src/packs/index.ts +5 -1
- package/src/packs/lockfile.ts +70 -5
- package/src/packs/pack-installer.ts +78 -2
- package/src/packs/pack-lifecycle.ts +115 -0
- package/src/packs/pack-lockfile.test.ts +1 -1
- package/src/packs/pack-system.test.ts +1 -1
- package/src/packs/types.ts +40 -2
- package/src/persistence/sqlite-provider.ts +27 -2
- package/src/planning/github-projection.ts +48 -44
- package/src/planning/plan-lifecycle.ts +14 -1
- package/src/queue/pipeline-runner.ts +4 -0
- package/src/runtime/admin-setup-ops.test.ts +9 -4
- package/src/runtime/curator-extra-ops.test.ts +7 -0
- package/src/runtime/curator-extra-ops.ts +10 -1
- package/src/runtime/facades/curator-facade.test.ts +7 -0
- package/src/runtime/facades/memory-facade.ts +187 -0
- package/src/runtime/orchestrate-ops.ts +156 -4
- package/src/runtime/runtime.test.ts +50 -2
- package/src/runtime/runtime.ts +132 -89
- package/src/runtime/session-briefing.test.ts +94 -2
- package/src/runtime/session-briefing.ts +48 -12
- package/src/runtime/shutdown-registry.test.ts +151 -0
- package/src/runtime/shutdown-registry.ts +85 -0
- package/src/runtime/types.ts +10 -1
- package/src/subagent/concurrency-manager.ts +89 -0
- package/src/subagent/dispatcher.ts +326 -0
- package/src/subagent/index.ts +28 -0
- package/src/subagent/orphan-reaper.ts +82 -0
- package/src/subagent/result-aggregator.ts +66 -0
- package/src/subagent/task-checkout.ts +60 -0
- package/src/subagent/types.ts +138 -0
- package/src/subagent/workspace-resolver.ts +117 -0
- package/src/transport/http-server.ts +50 -3
- package/src/transport/ws-server.ts +8 -0
- package/src/vault/linking.test.ts +12 -0
- package/src/vault/linking.ts +90 -44
- package/src/vault/vault-maintenance.ts +11 -18
- package/src/vault/vault-memories.ts +21 -13
- package/src/vault/vault-scaling.test.ts +3 -2
- package/src/vault/vault-schema.ts +21 -0
- package/src/vault/vault.ts +8 -3
- package/vitest.config.ts +2 -0
|
@@ -22,6 +22,7 @@ import type { Vault } from '../vault/vault.js';
|
|
|
22
22
|
import type { PluginRegistry } from '../plugins/plugin-registry.js';
|
|
23
23
|
import type { PluginContext } from '../plugins/types.js';
|
|
24
24
|
import type { PackRuntime } from '../domain-packs/pack-runtime.js';
|
|
25
|
+
import { PackLifecycleManager } from './pack-lifecycle.js';
|
|
25
26
|
|
|
26
27
|
const MANIFEST_FILENAME = 'soleri-pack.json';
|
|
27
28
|
|
|
@@ -31,6 +32,7 @@ const MANIFEST_FILENAME = 'soleri-pack.json';
|
|
|
31
32
|
|
|
32
33
|
export class PackInstaller {
|
|
33
34
|
private packs = new Map<string, InstalledPack>();
|
|
35
|
+
readonly lifecycle = new PackLifecycleManager();
|
|
34
36
|
|
|
35
37
|
constructor(
|
|
36
38
|
private vault: Vault,
|
|
@@ -206,17 +208,21 @@ export class PackInstaller {
|
|
|
206
208
|
const hooksDir = join(packDir, manifest.hooks?.dir ?? 'hooks');
|
|
207
209
|
const hooks = existsSync(hooksDir) ? listMarkdownFiles(hooksDir) : [];
|
|
208
210
|
|
|
209
|
-
// Track installed pack
|
|
211
|
+
// Track installed pack with lifecycle
|
|
212
|
+
this.lifecycle.initState(manifest.id, 'installed');
|
|
213
|
+
this.lifecycle.transition(manifest.id, 'ready', 'Initial install');
|
|
214
|
+
|
|
210
215
|
const installed: InstalledPack = {
|
|
211
216
|
id: manifest.id,
|
|
212
217
|
manifest,
|
|
213
218
|
directory: packDir,
|
|
214
|
-
status: '
|
|
219
|
+
status: 'ready',
|
|
215
220
|
vaultEntries,
|
|
216
221
|
skills,
|
|
217
222
|
hooks,
|
|
218
223
|
facadesRegistered,
|
|
219
224
|
installedAt: Date.now(),
|
|
225
|
+
transitions: this.lifecycle.getTransitions(manifest.id),
|
|
220
226
|
};
|
|
221
227
|
this.packs.set(manifest.id, installed);
|
|
222
228
|
|
|
@@ -231,17 +237,23 @@ export class PackInstaller {
|
|
|
231
237
|
} catch (e) {
|
|
232
238
|
const error = e instanceof Error ? e.message : String(e);
|
|
233
239
|
|
|
240
|
+
// Transition to error state
|
|
241
|
+
this.lifecycle.initState(manifest.id, 'installed');
|
|
242
|
+
this.lifecycle.transition(manifest.id, 'error', error);
|
|
243
|
+
|
|
234
244
|
this.packs.set(manifest.id, {
|
|
235
245
|
id: manifest.id,
|
|
236
246
|
manifest,
|
|
237
247
|
directory: packDir,
|
|
238
248
|
status: 'error',
|
|
239
249
|
error,
|
|
250
|
+
errorMessage: error,
|
|
240
251
|
vaultEntries: 0,
|
|
241
252
|
skills: [],
|
|
242
253
|
hooks: [],
|
|
243
254
|
facadesRegistered: false,
|
|
244
255
|
installedAt: Date.now(),
|
|
256
|
+
transitions: this.lifecycle.getTransitions(manifest.id),
|
|
245
257
|
});
|
|
246
258
|
|
|
247
259
|
return {
|
|
@@ -269,11 +281,75 @@ export class PackInstaller {
|
|
|
269
281
|
this.pluginRegistry.deactivate(packId);
|
|
270
282
|
}
|
|
271
283
|
|
|
284
|
+
// Transition to uninstalled
|
|
285
|
+
try {
|
|
286
|
+
this.lifecycle.transition(packId, 'uninstalled', 'User uninstall');
|
|
287
|
+
} catch {
|
|
288
|
+
// May not be tracked in lifecycle — continue anyway
|
|
289
|
+
}
|
|
290
|
+
this.lifecycle.remove(packId);
|
|
291
|
+
|
|
272
292
|
pack.status = 'uninstalled';
|
|
273
293
|
this.packs.delete(packId);
|
|
274
294
|
return true;
|
|
275
295
|
}
|
|
276
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Disable a pack — deactivates capabilities but preserves vault entries.
|
|
299
|
+
*/
|
|
300
|
+
disable(packId: string): boolean {
|
|
301
|
+
const pack = this.packs.get(packId);
|
|
302
|
+
if (!pack) return false;
|
|
303
|
+
|
|
304
|
+
this.lifecycle.transition(packId, 'disabled', 'User disabled');
|
|
305
|
+
|
|
306
|
+
// Deactivate facades
|
|
307
|
+
if (pack.facadesRegistered) {
|
|
308
|
+
this.pluginRegistry.deactivate(packId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
pack.status = 'disabled';
|
|
312
|
+
pack.disabledAt = Date.now();
|
|
313
|
+
pack.transitions = this.lifecycle.getTransitions(packId);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Enable a previously disabled pack — reactivates capabilities.
|
|
319
|
+
*/
|
|
320
|
+
async enable(packId: string, runtimeCtx?: unknown, packRuntime?: PackRuntime): Promise<boolean> {
|
|
321
|
+
const pack = this.packs.get(packId);
|
|
322
|
+
if (!pack) return false;
|
|
323
|
+
|
|
324
|
+
this.lifecycle.transition(packId, 'ready', 'User enabled');
|
|
325
|
+
|
|
326
|
+
// Reactivate facades
|
|
327
|
+
if (pack.manifest.facades.length > 0 && this.pluginRegistry.get(packId)) {
|
|
328
|
+
const ctx: PluginContext = {
|
|
329
|
+
packRuntime:
|
|
330
|
+
packRuntime ??
|
|
331
|
+
({
|
|
332
|
+
vault: {},
|
|
333
|
+
getProject: () => undefined,
|
|
334
|
+
listProjects: () => [],
|
|
335
|
+
createCheck: () => '',
|
|
336
|
+
validateCheck: () => null,
|
|
337
|
+
validateAndConsume: () => null,
|
|
338
|
+
} as unknown as PackRuntime),
|
|
339
|
+
runtime: runtimeCtx ?? {},
|
|
340
|
+
manifest: this.pluginRegistry.get(packId)!.manifest,
|
|
341
|
+
directory: pack.directory,
|
|
342
|
+
};
|
|
343
|
+
await this.pluginRegistry.activate(packId, ctx);
|
|
344
|
+
pack.facadesRegistered = true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
pack.status = 'ready';
|
|
348
|
+
pack.disabledAt = undefined;
|
|
349
|
+
pack.transitions = this.lifecycle.getTransitions(packId);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
277
353
|
/**
|
|
278
354
|
* Get an installed pack by ID.
|
|
279
355
|
*/
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack Lifecycle Manager — Central state machine for pack lifecycle management.
|
|
3
|
+
*
|
|
4
|
+
* Manages in-memory pack states, validates transitions against VALID_TRANSITIONS,
|
|
5
|
+
* records transition history, and notifies listeners on state changes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PackState, PackTransition } from './types.js';
|
|
9
|
+
import { VALID_TRANSITIONS } from './types.js';
|
|
10
|
+
|
|
11
|
+
type TransitionListener = (packId: string, from: PackState, to: PackState, reason?: string) => void;
|
|
12
|
+
|
|
13
|
+
interface PackEntry {
|
|
14
|
+
state: PackState;
|
|
15
|
+
transitions: PackTransition[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PackLifecycleManager {
|
|
19
|
+
private packs: Map<string, PackEntry> = new Map();
|
|
20
|
+
private listeners: Array<TransitionListener> = [];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Transition a pack to a new state. Validates against VALID_TRANSITIONS.
|
|
24
|
+
* Throws a descriptive error if the transition is not allowed.
|
|
25
|
+
*/
|
|
26
|
+
transition(packId: string, to: PackState, reason?: string): void {
|
|
27
|
+
const entry = this.packs.get(packId);
|
|
28
|
+
if (!entry) {
|
|
29
|
+
throw new Error(`Pack '${packId}' is not being tracked`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const currentState = entry.state;
|
|
33
|
+
const validTargets = VALID_TRANSITIONS[currentState];
|
|
34
|
+
|
|
35
|
+
if (!validTargets.includes(to)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid pack lifecycle transition for '${packId}': '${currentState}' \u2192 '${to}'. Valid targets from '${currentState}': ${validTargets.join(', ')}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const transition: PackTransition = {
|
|
42
|
+
from: currentState,
|
|
43
|
+
to,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
reason,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
entry.state = to;
|
|
49
|
+
entry.transitions.push(transition);
|
|
50
|
+
|
|
51
|
+
for (const listener of this.listeners) {
|
|
52
|
+
listener(packId, currentState, to, reason);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Set initial state without transition validation (for loading from lockfile).
|
|
58
|
+
*/
|
|
59
|
+
initState(packId: string, state: PackState): void {
|
|
60
|
+
this.packs.set(packId, { state, transitions: [] });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns current state of a pack, or undefined if not tracked.
|
|
65
|
+
*/
|
|
66
|
+
getState(packId: string): PackState | undefined {
|
|
67
|
+
return this.packs.get(packId)?.state;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the full transition history for a pack.
|
|
72
|
+
*/
|
|
73
|
+
getTransitions(packId: string): PackTransition[] {
|
|
74
|
+
return this.packs.get(packId)?.transitions ?? [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Register a listener for state transitions. Returns an unsubscribe function.
|
|
79
|
+
*/
|
|
80
|
+
onTransition(callback: TransitionListener): () => void {
|
|
81
|
+
this.listeners.push(callback);
|
|
82
|
+
return () => {
|
|
83
|
+
const idx = this.listeners.indexOf(callback);
|
|
84
|
+
if (idx !== -1) {
|
|
85
|
+
this.listeners.splice(idx, 1);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remove a pack from tracking entirely.
|
|
92
|
+
*/
|
|
93
|
+
remove(packId: string): void {
|
|
94
|
+
this.packs.delete(packId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* List all tracked packs with their current state.
|
|
99
|
+
*/
|
|
100
|
+
listAll(): Array<{ packId: string; state: PackState }> {
|
|
101
|
+
const result: Array<{ packId: string; state: PackState }> = [];
|
|
102
|
+
for (const [packId, entry] of this.packs) {
|
|
103
|
+
result.push({ packId, state: entry.state });
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Clear all tracked state and listeners.
|
|
110
|
+
*/
|
|
111
|
+
reset(): void {
|
|
112
|
+
this.packs.clear();
|
|
113
|
+
this.listeners.length = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -81,7 +81,7 @@ describe('PackLockfile', () => {
|
|
|
81
81
|
expect(existsSync(lockPath)).toBe(true);
|
|
82
82
|
|
|
83
83
|
const data = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
84
|
-
expect(data.version).toBe(
|
|
84
|
+
expect(data.version).toBe(2);
|
|
85
85
|
expect(data.packs['test-pack'].version).toBe('1.0.0');
|
|
86
86
|
});
|
|
87
87
|
|
|
@@ -417,7 +417,7 @@ describe('PackInstaller', () => {
|
|
|
417
417
|
const pack = installer.get('specific');
|
|
418
418
|
expect(pack).toBeDefined();
|
|
419
419
|
expect(pack?.manifest.version).toBe('3.0.0');
|
|
420
|
-
expect(pack?.status).toBe('
|
|
420
|
+
expect(pack?.status).toBe('ready');
|
|
421
421
|
});
|
|
422
422
|
});
|
|
423
423
|
});
|
package/src/packs/types.ts
CHANGED
|
@@ -108,13 +108,43 @@ export type PackManifest = z.infer<typeof packManifestSchema>;
|
|
|
108
108
|
// INSTALL TYPES
|
|
109
109
|
// =============================================================================
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
// ─── Lifecycle States ─────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/** Full lifecycle state for a pack */
|
|
114
|
+
export type PackState =
|
|
115
|
+
| 'installed' // Just installed, not yet activated
|
|
116
|
+
| 'ready' // Active — capabilities, skills, hooks all live
|
|
117
|
+
| 'disabled' // Temporarily deactivated — vault entries kept, capabilities off
|
|
118
|
+
| 'error' // Failed to activate — error message in errorMessage field
|
|
119
|
+
| 'upgrade_pending' // New version available, old version still active
|
|
120
|
+
| 'uninstalled'; // Removed — vault entries remain (permanent knowledge)
|
|
121
|
+
|
|
122
|
+
/** @deprecated Use PackState instead */
|
|
123
|
+
export type PackStatus = PackState;
|
|
124
|
+
|
|
125
|
+
/** Valid state transitions — key is "from" state, values are allowed "to" states */
|
|
126
|
+
export const VALID_TRANSITIONS: Record<PackState, PackState[]> = {
|
|
127
|
+
installed: ['ready', 'error', 'uninstalled'],
|
|
128
|
+
ready: ['ready', 'disabled', 'error', 'upgrade_pending', 'uninstalled'],
|
|
129
|
+
disabled: ['ready', 'uninstalled'],
|
|
130
|
+
error: ['ready', 'uninstalled'],
|
|
131
|
+
upgrade_pending: ['ready', 'error', 'uninstalled'],
|
|
132
|
+
uninstalled: ['installed'],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** A recorded state transition */
|
|
136
|
+
export interface PackTransition {
|
|
137
|
+
from: PackState;
|
|
138
|
+
to: PackState;
|
|
139
|
+
timestamp: number;
|
|
140
|
+
reason?: string;
|
|
141
|
+
}
|
|
112
142
|
|
|
113
143
|
export interface InstalledPack {
|
|
114
144
|
id: string;
|
|
115
145
|
manifest: PackManifest;
|
|
116
146
|
directory: string;
|
|
117
|
-
status:
|
|
147
|
+
status: PackState;
|
|
118
148
|
error?: string;
|
|
119
149
|
/** Number of vault entries seeded */
|
|
120
150
|
vaultEntries: number;
|
|
@@ -125,6 +155,14 @@ export interface InstalledPack {
|
|
|
125
155
|
/** Whether facades were registered via plugin system */
|
|
126
156
|
facadesRegistered: boolean;
|
|
127
157
|
installedAt: number;
|
|
158
|
+
/** Lifecycle transition history (most recent last) */
|
|
159
|
+
transitions?: PackTransition[];
|
|
160
|
+
/** When the pack was disabled (if in disabled state) */
|
|
161
|
+
disabledAt?: number;
|
|
162
|
+
/** Error message (if in error state) */
|
|
163
|
+
errorMessage?: string;
|
|
164
|
+
/** Version available for upgrade (if in upgrade_pending state) */
|
|
165
|
+
upgradeVersion?: string;
|
|
128
166
|
}
|
|
129
167
|
|
|
130
168
|
export interface InstallResult {
|
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Supports both positional (array) and named (object) parameters.
|
|
5
5
|
* Exposes getDatabase() for backward-compat consumers that need the raw db.
|
|
6
|
+
*
|
|
7
|
+
* better-sqlite3 is loaded lazily at construction time (not at module import)
|
|
8
|
+
* so that code paths that never instantiate a provider don't require the
|
|
9
|
+
* native module to be installed.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
import Database from 'better-sqlite3';
|
|
12
|
+
import type Database from 'better-sqlite3';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
9
14
|
import { mkdirSync } from 'node:fs';
|
|
10
15
|
import { dirname } from 'node:path';
|
|
11
16
|
import type {
|
|
@@ -15,11 +20,30 @@ import type {
|
|
|
15
20
|
FtsSearchOptions,
|
|
16
21
|
} from './types.js';
|
|
17
22
|
|
|
23
|
+
type DatabaseConstructor = typeof Database;
|
|
24
|
+
let _DatabaseCtor: DatabaseConstructor | undefined;
|
|
25
|
+
|
|
26
|
+
function loadDriver(): DatabaseConstructor {
|
|
27
|
+
if (!_DatabaseCtor) {
|
|
28
|
+
const req = createRequire(import.meta.url);
|
|
29
|
+
try {
|
|
30
|
+
_DatabaseCtor = req('better-sqlite3') as DatabaseConstructor;
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'better-sqlite3 is required for persistence but is not installed.\n' +
|
|
34
|
+
'Run: npm install better-sqlite3',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return _DatabaseCtor;
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
/** Apply performance-tuning PRAGMAs for file-backed SQLite databases. */
|
|
19
42
|
export function applyPerformancePragmas(db: Database.Database): void {
|
|
20
43
|
db.pragma('cache_size = -64000'); // 64MB
|
|
21
44
|
db.pragma('temp_store = MEMORY');
|
|
22
45
|
db.pragma('mmap_size = 268435456'); // 256MB
|
|
46
|
+
db.pragma('synchronous = NORMAL');
|
|
23
47
|
}
|
|
24
48
|
|
|
25
49
|
export class SQLitePersistenceProvider implements PersistenceProvider {
|
|
@@ -27,8 +51,9 @@ export class SQLitePersistenceProvider implements PersistenceProvider {
|
|
|
27
51
|
private db: Database.Database;
|
|
28
52
|
|
|
29
53
|
constructor(path: string = ':memory:') {
|
|
54
|
+
const Driver = loadDriver();
|
|
30
55
|
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
|
|
31
|
-
this.db = new
|
|
56
|
+
this.db = new Driver(path);
|
|
32
57
|
if (path !== ':memory:') applyPerformancePragmas(this.db);
|
|
33
58
|
}
|
|
34
59
|
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* The plan is the source of truth; GitHub issues are the projection.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { execFile } from 'node:child_process';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
11
14
|
|
|
12
15
|
// ---------------------------------------------------------------------------
|
|
13
16
|
// Types
|
|
@@ -88,15 +91,14 @@ export function parseGitHubRemote(remoteUrl: string): GitHubRepo | null {
|
|
|
88
91
|
* Detect the GitHub remote from a project directory.
|
|
89
92
|
* Returns null if no GitHub remote found or not a git repo.
|
|
90
93
|
*/
|
|
91
|
-
export function detectGitHubRemote(projectPath: string): GitHubRepo | null {
|
|
94
|
+
export async function detectGitHubRemote(projectPath: string): Promise<GitHubRepo | null> {
|
|
92
95
|
try {
|
|
93
|
-
const
|
|
96
|
+
const { stdout } = await execFileAsync('git', ['remote', 'get-url', 'origin'], {
|
|
94
97
|
cwd: projectPath,
|
|
95
|
-
encoding: 'utf-8',
|
|
96
98
|
timeout: 5000,
|
|
97
|
-
|
|
98
|
-
})
|
|
99
|
-
return parseGitHubRemote(
|
|
99
|
+
signal: AbortSignal.timeout(5000),
|
|
100
|
+
});
|
|
101
|
+
return parseGitHubRemote(stdout.trim());
|
|
100
102
|
} catch {
|
|
101
103
|
return null;
|
|
102
104
|
}
|
|
@@ -105,12 +107,11 @@ export function detectGitHubRemote(projectPath: string): GitHubRepo | null {
|
|
|
105
107
|
/**
|
|
106
108
|
* Check if the `gh` CLI is authenticated.
|
|
107
109
|
*/
|
|
108
|
-
export function isGhAuthenticated(): boolean {
|
|
110
|
+
export async function isGhAuthenticated(): Promise<boolean> {
|
|
109
111
|
try {
|
|
110
|
-
|
|
111
|
-
encoding: 'utf-8',
|
|
112
|
+
await execFileAsync('gh', ['auth', 'status'], {
|
|
112
113
|
timeout: 5000,
|
|
113
|
-
|
|
114
|
+
signal: AbortSignal.timeout(5000),
|
|
114
115
|
});
|
|
115
116
|
return true;
|
|
116
117
|
} catch {
|
|
@@ -125,9 +126,9 @@ export function isGhAuthenticated(): boolean {
|
|
|
125
126
|
/**
|
|
126
127
|
* List milestones for a GitHub repo.
|
|
127
128
|
*/
|
|
128
|
-
export function listMilestones(repo: GitHubRepo): GitHubMilestone[] {
|
|
129
|
+
export async function listMilestones(repo: GitHubRepo): Promise<GitHubMilestone[]> {
|
|
129
130
|
try {
|
|
130
|
-
const
|
|
131
|
+
const { stdout } = await execFileAsync(
|
|
131
132
|
'gh',
|
|
132
133
|
[
|
|
133
134
|
'api',
|
|
@@ -136,12 +137,12 @@ export function listMilestones(repo: GitHubRepo): GitHubMilestone[] {
|
|
|
136
137
|
'.[] | {number, title, state}',
|
|
137
138
|
],
|
|
138
139
|
{
|
|
139
|
-
encoding: 'utf-8',
|
|
140
140
|
timeout: 10000,
|
|
141
|
-
|
|
141
|
+
signal: AbortSignal.timeout(10000),
|
|
142
142
|
},
|
|
143
|
-
)
|
|
143
|
+
);
|
|
144
144
|
|
|
145
|
+
const output = stdout.trim();
|
|
145
146
|
if (!output) return [];
|
|
146
147
|
|
|
147
148
|
// gh --jq outputs one JSON object per line
|
|
@@ -157,9 +158,12 @@ export function listMilestones(repo: GitHubRepo): GitHubMilestone[] {
|
|
|
157
158
|
/**
|
|
158
159
|
* List open issues for a GitHub repo.
|
|
159
160
|
*/
|
|
160
|
-
export function listOpenIssues(
|
|
161
|
+
export async function listOpenIssues(
|
|
162
|
+
repo: GitHubRepo,
|
|
163
|
+
limit: number = 100,
|
|
164
|
+
): Promise<GitHubIssue[]> {
|
|
161
165
|
try {
|
|
162
|
-
const
|
|
166
|
+
const { stdout } = await execFileAsync(
|
|
163
167
|
'gh',
|
|
164
168
|
[
|
|
165
169
|
'issue',
|
|
@@ -174,12 +178,12 @@ export function listOpenIssues(repo: GitHubRepo, limit: number = 100): GitHubIss
|
|
|
174
178
|
'number,title,state,body',
|
|
175
179
|
],
|
|
176
180
|
{
|
|
177
|
-
encoding: 'utf-8',
|
|
178
181
|
timeout: 10000,
|
|
179
|
-
|
|
182
|
+
signal: AbortSignal.timeout(10000),
|
|
180
183
|
},
|
|
181
|
-
)
|
|
184
|
+
);
|
|
182
185
|
|
|
186
|
+
const output = stdout.trim();
|
|
183
187
|
if (!output) return [];
|
|
184
188
|
return JSON.parse(output) as GitHubIssue[];
|
|
185
189
|
} catch {
|
|
@@ -190,18 +194,18 @@ export function listOpenIssues(repo: GitHubRepo, limit: number = 100): GitHubIss
|
|
|
190
194
|
/**
|
|
191
195
|
* List labels for a GitHub repo.
|
|
192
196
|
*/
|
|
193
|
-
export function listLabels(repo: GitHubRepo): GitHubLabel[] {
|
|
197
|
+
export async function listLabels(repo: GitHubRepo): Promise<GitHubLabel[]> {
|
|
194
198
|
try {
|
|
195
|
-
const
|
|
199
|
+
const { stdout } = await execFileAsync(
|
|
196
200
|
'gh',
|
|
197
201
|
['label', 'list', '--repo', `${repo.owner}/${repo.repo}`, '--json', 'name,color'],
|
|
198
202
|
{
|
|
199
|
-
encoding: 'utf-8',
|
|
200
203
|
timeout: 10000,
|
|
201
|
-
|
|
204
|
+
signal: AbortSignal.timeout(10000),
|
|
202
205
|
},
|
|
203
|
-
)
|
|
206
|
+
);
|
|
204
207
|
|
|
208
|
+
const output = stdout.trim();
|
|
205
209
|
if (!output) return [];
|
|
206
210
|
return JSON.parse(output) as GitHubLabel[];
|
|
207
211
|
} catch {
|
|
@@ -217,16 +221,18 @@ export function listLabels(repo: GitHubRepo): GitHubLabel[] {
|
|
|
217
221
|
* Detect full GitHub context for a project.
|
|
218
222
|
* Returns null if not a GitHub project or gh CLI not available.
|
|
219
223
|
*/
|
|
220
|
-
export function detectGitHubContext(projectPath: string): GitHubContext | null {
|
|
221
|
-
const repo = detectGitHubRemote(projectPath);
|
|
224
|
+
export async function detectGitHubContext(projectPath: string): Promise<GitHubContext | null> {
|
|
225
|
+
const repo = await detectGitHubRemote(projectPath);
|
|
222
226
|
if (!repo) return null;
|
|
223
227
|
|
|
224
|
-
const authenticated = isGhAuthenticated();
|
|
228
|
+
const authenticated = await isGhAuthenticated();
|
|
225
229
|
if (!authenticated) return null;
|
|
226
230
|
|
|
227
|
-
const milestones =
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
const [milestones, existingIssues, labels] = await Promise.all([
|
|
232
|
+
listMilestones(repo),
|
|
233
|
+
listOpenIssues(repo),
|
|
234
|
+
listLabels(repo),
|
|
235
|
+
]);
|
|
230
236
|
|
|
231
237
|
return { repo, authenticated, milestones, existingIssues, labels };
|
|
232
238
|
}
|
|
@@ -370,7 +376,7 @@ export function formatIssueBody(
|
|
|
370
376
|
* Create a GitHub issue using the `gh` CLI.
|
|
371
377
|
* Returns the issue number, or null on failure.
|
|
372
378
|
*/
|
|
373
|
-
export function createGitHubIssue(
|
|
379
|
+
export async function createGitHubIssue(
|
|
374
380
|
repo: GitHubRepo,
|
|
375
381
|
title: string,
|
|
376
382
|
body: string,
|
|
@@ -378,7 +384,7 @@ export function createGitHubIssue(
|
|
|
378
384
|
milestone?: number;
|
|
379
385
|
labels?: string[];
|
|
380
386
|
},
|
|
381
|
-
): number | null {
|
|
387
|
+
): Promise<number | null> {
|
|
382
388
|
try {
|
|
383
389
|
const args = [
|
|
384
390
|
'issue',
|
|
@@ -399,14 +405,13 @@ export function createGitHubIssue(
|
|
|
399
405
|
args.push('--label', options.labels.join(','));
|
|
400
406
|
}
|
|
401
407
|
|
|
402
|
-
const
|
|
403
|
-
encoding: 'utf-8',
|
|
408
|
+
const { stdout } = await execFileAsync('gh', args, {
|
|
404
409
|
timeout: 15000,
|
|
405
|
-
|
|
406
|
-
})
|
|
410
|
+
signal: AbortSignal.timeout(15000),
|
|
411
|
+
});
|
|
407
412
|
|
|
408
413
|
// gh issue create returns the issue URL: https://github.com/owner/repo/issues/123
|
|
409
|
-
const match =
|
|
414
|
+
const match = stdout.trim().match(/\/issues\/(\d+)/);
|
|
410
415
|
return match ? parseInt(match[1], 10) : null;
|
|
411
416
|
} catch {
|
|
412
417
|
return null;
|
|
@@ -416,13 +421,13 @@ export function createGitHubIssue(
|
|
|
416
421
|
/**
|
|
417
422
|
* Update an existing GitHub issue body (for linking plans to existing issues).
|
|
418
423
|
*/
|
|
419
|
-
export function updateGitHubIssueBody(
|
|
424
|
+
export async function updateGitHubIssueBody(
|
|
420
425
|
repo: GitHubRepo,
|
|
421
426
|
issueNumber: number,
|
|
422
427
|
body: string,
|
|
423
|
-
): boolean {
|
|
428
|
+
): Promise<boolean> {
|
|
424
429
|
try {
|
|
425
|
-
|
|
430
|
+
await execFileAsync(
|
|
426
431
|
'gh',
|
|
427
432
|
[
|
|
428
433
|
'issue',
|
|
@@ -434,9 +439,8 @@ export function updateGitHubIssueBody(
|
|
|
434
439
|
body,
|
|
435
440
|
],
|
|
436
441
|
{
|
|
437
|
-
encoding: 'utf-8',
|
|
438
442
|
timeout: 15000,
|
|
439
|
-
|
|
443
|
+
signal: AbortSignal.timeout(15000),
|
|
440
444
|
},
|
|
441
445
|
);
|
|
442
446
|
return true;
|
|
@@ -380,7 +380,20 @@ export function createPlanObject(params: {
|
|
|
380
380
|
scope: params.scope,
|
|
381
381
|
status: params.initialStatus ?? 'draft',
|
|
382
382
|
decisions: params.decisions ?? [],
|
|
383
|
-
tasks: (params.tasks ?? []).map((t, i) =>
|
|
383
|
+
tasks: (params.tasks ?? []).map((t, i) =>
|
|
384
|
+
Object.assign(
|
|
385
|
+
{
|
|
386
|
+
id: `task-${i + 1}`,
|
|
387
|
+
title: t.title,
|
|
388
|
+
description: t.description,
|
|
389
|
+
status: `pending` as TaskStatus,
|
|
390
|
+
},
|
|
391
|
+
t.phase !== undefined && { phase: t.phase },
|
|
392
|
+
t.milestone !== undefined && { milestone: t.milestone },
|
|
393
|
+
t.parentTaskId !== undefined && { parentTaskId: t.parentTaskId },
|
|
394
|
+
{ updatedAt: now },
|
|
395
|
+
),
|
|
396
|
+
),
|
|
384
397
|
...(params.approach !== undefined && { approach: params.approach }),
|
|
385
398
|
...(params.context !== undefined && { context: params.context }),
|
|
386
399
|
...(params.success_criteria !== undefined && { success_criteria: params.success_criteria }),
|
|
@@ -58,6 +58,10 @@ export class PipelineRunner {
|
|
|
58
58
|
if (this.running) return;
|
|
59
59
|
this.running = true;
|
|
60
60
|
this.timer = setInterval(() => this.tick(), this.pollIntervalMs);
|
|
61
|
+
// Don't prevent process exit for background polling
|
|
62
|
+
if (this.timer && typeof this.timer === 'object' && 'unref' in this.timer) {
|
|
63
|
+
(this.timer as NodeJS.Timeout).unref();
|
|
64
|
+
}
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
/**
|
|
@@ -5,17 +5,21 @@ import type { OpDefinition } from '../facades/types.js';
|
|
|
5
5
|
|
|
6
6
|
// ─── Mock Node.js fs/os modules ────────────────────────────────────────
|
|
7
7
|
|
|
8
|
+
/** Normalize path separators so Windows backslash paths match forward-slash keys */
|
|
9
|
+
const norm = (p: string): string => p.replace(/\\/g, '/');
|
|
10
|
+
|
|
8
11
|
const mockFs: Record<string, string> = {};
|
|
9
12
|
const mockDirs = new Set<string>();
|
|
10
13
|
|
|
11
14
|
vi.mock('node:fs', () => ({
|
|
12
|
-
existsSync: vi.fn((p: string) => p in mockFs || mockDirs.has(p)),
|
|
15
|
+
existsSync: vi.fn((p: string) => norm(p) in mockFs || mockDirs.has(norm(p))),
|
|
13
16
|
readFileSync: vi.fn((p: string) => {
|
|
14
|
-
|
|
17
|
+
const key = norm(p);
|
|
18
|
+
if (key in mockFs) return mockFs[key];
|
|
15
19
|
throw new Error(`ENOENT: ${p}`);
|
|
16
20
|
}),
|
|
17
21
|
writeFileSync: vi.fn((p: string, content: string) => {
|
|
18
|
-
mockFs[p] = content;
|
|
22
|
+
mockFs[norm(p)] = content;
|
|
19
23
|
}),
|
|
20
24
|
mkdirSync: vi.fn((_p: string) => undefined),
|
|
21
25
|
copyFileSync: vi.fn(),
|
|
@@ -30,7 +34,8 @@ vi.mock('node:os', () => ({
|
|
|
30
34
|
|
|
31
35
|
vi.mock('node:path', async () => {
|
|
32
36
|
const actual = await vi.importActual<typeof import('node:path')>('node:path');
|
|
33
|
-
|
|
37
|
+
// Always use posix path semantics so mock filesystem keys (forward slashes) work on all platforms
|
|
38
|
+
return { ...actual.posix, default: actual.posix };
|
|
34
39
|
});
|
|
35
40
|
|
|
36
41
|
vi.mock('./claude-md-helpers.js', () => ({
|