@soleri/core 9.14.4 → 9.16.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/data/flows/deliver.flow.yaml +11 -0
- package/data/flows/design.flow.yaml +4 -14
- package/data/flows/enhance.flow.yaml +10 -0
- package/data/flows/explore.flow.yaml +16 -0
- package/data/flows/fix.flow.yaml +1 -1
- package/data/flows/review.flow.yaml +13 -4
- package/dist/brain/brain.d.ts +9 -0
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +11 -1
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +24 -0
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/capabilities/chain-mapping.d.ts.map +1 -1
- package/dist/capabilities/chain-mapping.js +5 -4
- package/dist/capabilities/chain-mapping.js.map +1 -1
- package/dist/capabilities/registry.d.ts +6 -0
- package/dist/capabilities/registry.d.ts.map +1 -1
- package/dist/capabilities/registry.js +3 -2
- package/dist/capabilities/registry.js.map +1 -1
- package/dist/chat/chat-session.d.ts +6 -0
- package/dist/chat/chat-session.d.ts.map +1 -1
- package/dist/chat/chat-session.js +68 -17
- package/dist/chat/chat-session.js.map +1 -1
- package/dist/context/context-engine.js +1 -1
- package/dist/context/context-engine.js.map +1 -1
- package/dist/curator/curator.d.ts +6 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +138 -0
- package/dist/curator/curator.js.map +1 -1
- package/dist/curator/types.d.ts +10 -0
- package/dist/curator/types.d.ts.map +1 -1
- package/dist/engine/bin/soleri-engine.js +0 -0
- package/dist/engine/core-ops.d.ts.map +1 -1
- package/dist/engine/core-ops.js +38 -1
- package/dist/engine/core-ops.js.map +1 -1
- package/dist/flows/epilogue.d.ts +5 -1
- package/dist/flows/epilogue.d.ts.map +1 -1
- package/dist/flows/epilogue.js +11 -3
- package/dist/flows/epilogue.js.map +1 -1
- package/dist/flows/executor.d.ts.map +1 -1
- package/dist/flows/executor.js +13 -5
- package/dist/flows/executor.js.map +1 -1
- package/dist/flows/index.d.ts +1 -2
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js +1 -0
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/plan-builder.d.ts +17 -1
- package/dist/flows/plan-builder.d.ts.map +1 -1
- package/dist/flows/plan-builder.js +67 -6
- package/dist/flows/plan-builder.js.map +1 -1
- package/dist/flows/probes.d.ts +1 -1
- package/dist/flows/probes.d.ts.map +1 -1
- package/dist/flows/probes.js +15 -3
- package/dist/flows/probes.js.map +1 -1
- package/dist/flows/types.d.ts +47 -20
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/flows/types.js +6 -1
- package/dist/flows/types.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/intake/content-classifier.d.ts +10 -4
- package/dist/intake/content-classifier.d.ts.map +1 -1
- package/dist/intake/content-classifier.js +19 -5
- package/dist/intake/content-classifier.js.map +1 -1
- package/dist/intake/text-ingester.d.ts +18 -0
- package/dist/intake/text-ingester.d.ts.map +1 -1
- package/dist/intake/text-ingester.js +37 -13
- package/dist/intake/text-ingester.js.map +1 -1
- package/dist/packs/pack-installer.d.ts.map +1 -1
- package/dist/packs/pack-installer.js +28 -2
- package/dist/packs/pack-installer.js.map +1 -1
- package/dist/planning/planner-types.d.ts +2 -0
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/planning/planner.d.ts +4 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +50 -4
- package/dist/planning/planner.js.map +1 -1
- package/dist/playbooks/playbook-executor.d.ts +10 -1
- package/dist/playbooks/playbook-executor.d.ts.map +1 -1
- package/dist/playbooks/playbook-executor.js +8 -2
- package/dist/playbooks/playbook-executor.js.map +1 -1
- package/dist/playbooks/playbook-types.d.ts +8 -0
- package/dist/playbooks/playbook-types.d.ts.map +1 -1
- package/dist/plugins/types.d.ts +2 -2
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.js +30 -0
- package/dist/runtime/admin-extra-ops.js.map +1 -1
- package/dist/runtime/admin-ops.d.ts.map +1 -1
- package/dist/runtime/admin-ops.js +60 -21
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts +11 -0
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +146 -37
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +38 -12
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +16 -4
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/context-facade.d.ts.map +1 -1
- package/dist/runtime/facades/context-facade.js +9 -3
- package/dist/runtime/facades/context-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +20 -7
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
- package/dist/runtime/facades/orchestrate-facade.js +40 -1
- package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +113 -4
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
- package/dist/runtime/facades/vault-facade.js +24 -3
- package/dist/runtime/facades/vault-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts +21 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +132 -38
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +16 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/schema-helpers.d.ts.map +1 -1
- package/dist/runtime/schema-helpers.js +4 -0
- package/dist/runtime/schema-helpers.js.map +1 -1
- package/dist/runtime/types.d.ts +19 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.js +16 -3
- package/dist/runtime/vault-linking-ops.js.map +1 -1
- package/dist/scheduler/cron-validator.d.ts +15 -0
- package/dist/scheduler/cron-validator.d.ts.map +1 -0
- package/dist/scheduler/cron-validator.js +93 -0
- package/dist/scheduler/cron-validator.js.map +1 -0
- package/dist/scheduler/platform-linux.d.ts +14 -0
- package/dist/scheduler/platform-linux.d.ts.map +1 -0
- package/dist/scheduler/platform-linux.js +107 -0
- package/dist/scheduler/platform-linux.js.map +1 -0
- package/dist/scheduler/platform-macos.d.ts +15 -0
- package/dist/scheduler/platform-macos.d.ts.map +1 -0
- package/dist/scheduler/platform-macos.js +131 -0
- package/dist/scheduler/platform-macos.js.map +1 -0
- package/dist/scheduler/scheduler-ops.d.ts +14 -0
- package/dist/scheduler/scheduler-ops.d.ts.map +1 -0
- package/dist/scheduler/scheduler-ops.js +77 -0
- package/dist/scheduler/scheduler-ops.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -0
- package/dist/scheduler/scheduler.d.ts.map +1 -0
- package/dist/scheduler/scheduler.js +144 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/types.d.ts +48 -0
- package/dist/scheduler/types.d.ts.map +1 -0
- package/dist/scheduler/types.js +6 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/skills/sync-skills.d.ts +11 -0
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +132 -38
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/skills/validate-skills.d.ts +32 -0
- package/dist/skills/validate-skills.d.ts.map +1 -0
- package/dist/skills/validate-skills.js +396 -0
- package/dist/skills/validate-skills.js.map +1 -0
- package/dist/utils/worktree-reaper.d.ts +38 -0
- package/dist/utils/worktree-reaper.d.ts.map +1 -0
- package/dist/utils/worktree-reaper.js +85 -0
- package/dist/utils/worktree-reaper.js.map +1 -0
- package/dist/vault/default-canonical-tags.d.ts +15 -0
- package/dist/vault/default-canonical-tags.d.ts.map +1 -0
- package/dist/vault/default-canonical-tags.js +65 -0
- package/dist/vault/default-canonical-tags.js.map +1 -0
- package/dist/vault/scope-detector.d.ts.map +1 -1
- package/dist/vault/scope-detector.js +37 -4
- package/dist/vault/scope-detector.js.map +1 -1
- package/dist/vault/tag-normalizer.d.ts +42 -0
- package/dist/vault/tag-normalizer.d.ts.map +1 -0
- package/dist/vault/tag-normalizer.js +157 -0
- package/dist/vault/tag-normalizer.js.map +1 -0
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +3 -1
- package/dist/vault/vault-entries.js.map +1 -1
- package/package.json +5 -1
- package/src/__tests__/embeddings.test.ts +3 -3
- package/src/agency/agency-manager.test.ts +4 -4
- package/src/agency/default-rules.test.ts +0 -13
- package/src/brain/brain-intelligence.test.ts +0 -5
- package/src/brain/brain.ts +25 -1
- package/src/brain/intelligence.ts +25 -0
- package/src/brain/second-brain-features.test.ts +2 -14
- package/src/brain/types.ts +1 -0
- package/src/capabilities/chain-mapping.test.ts +1 -6
- package/src/capabilities/chain-mapping.ts +6 -4
- package/src/capabilities/registry.test.ts +1 -1
- package/src/capabilities/registry.ts +9 -2
- package/src/chat/agent-loop.test.ts +1 -1
- package/src/chat/chat-enhanced.test.ts +0 -8
- package/src/chat/chat-session.ts +75 -17
- package/src/chat/chat-transport.test.ts +31 -1
- package/src/claudemd/compose.test.ts +0 -5
- package/src/context/context-engine.test.ts +0 -1
- package/src/context/context-engine.ts +1 -1
- package/src/control/intent-router.test.ts +2 -2
- package/src/curator/curator.ts +180 -0
- package/src/curator/tag-manager.test.ts +0 -4
- package/src/curator/types.ts +10 -0
- package/src/domain-packs/types.test.ts +0 -5
- package/src/dream/dream.test.ts +0 -7
- package/src/enforcement/registry.test.ts +2 -2
- package/src/engine/core-ops.test.ts +4 -22
- package/src/engine/core-ops.ts +36 -1
- package/src/engine/module-manifest.test.ts +1 -31
- package/src/engine/register-engine.test.ts +3 -33
- package/src/errors/retry.test.ts +3 -1
- package/src/flows/chain-runner.test.ts +0 -6
- package/src/flows/context-router.test.ts +3 -3
- package/src/flows/epilogue.test.ts +40 -2
- package/src/flows/epilogue.ts +11 -2
- package/src/flows/executor.test.ts +48 -2
- package/src/flows/executor.ts +15 -5
- package/src/flows/index.ts +1 -3
- package/src/flows/plan-builder.test.ts +201 -0
- package/src/flows/plan-builder.ts +81 -5
- package/src/flows/probes.ts +17 -3
- package/src/flows/types.ts +31 -2
- package/src/health/health-registry.test.ts +3 -1
- package/src/index.ts +24 -0
- package/src/intake/content-classifier.ts +22 -4
- package/src/intake/dedup-gate.test.ts +2 -6
- package/src/intake/text-ingester.test.ts +3 -4
- package/src/intake/text-ingester.ts +61 -12
- package/src/llm/llm-client.test.ts +1 -1
- package/src/llm/utils.test.ts +1 -1
- package/src/migrations/migration-runner.test.ts +0 -1
- package/src/operator/operator-context-store.test.ts +0 -13
- package/src/operator/operator-profile.test.ts +2 -20
- package/src/packs/pack-installer.ts +28 -2
- package/src/packs/pack-system.test.ts +2 -2
- package/src/persona/defaults.test.ts +19 -19
- package/src/planning/gap-passes.test.ts +0 -46
- package/src/planning/gap-patterns.test.ts +0 -42
- package/src/planning/goal-ancestry.test.ts +3 -1
- package/src/planning/plan-lifecycle.test.ts +15 -7
- package/src/planning/planner-types.ts +2 -0
- package/src/planning/planner.test.ts +86 -90
- package/src/planning/planner.ts +56 -4
- package/src/planning/reconciliation-engine.test.ts +3 -10
- package/src/planning/task-complexity-assessor.test.ts +0 -5
- package/src/planning/task-verifier.test.ts +3 -1
- package/src/playbooks/generic/generic-playbooks.test.ts +0 -28
- package/src/playbooks/index.test.ts +0 -55
- package/src/playbooks/playbook-executor.test.ts +76 -0
- package/src/playbooks/playbook-executor.ts +24 -3
- package/src/playbooks/playbook-types.ts +8 -0
- package/src/plugins/plugin-registry.test.ts +6 -2
- package/src/project/project-registry.test.ts +2 -0
- package/src/queue/async-infrastructure.test.ts +6 -4
- package/src/queue/job-queue.test.ts +13 -7
- package/src/runtime/admin-extra-ops.test.ts +35 -30
- package/src/runtime/admin-extra-ops.ts +30 -0
- package/src/runtime/admin-ops.test.ts +0 -4
- package/src/runtime/admin-ops.ts +63 -21
- package/src/runtime/admin-setup-ops.test.ts +229 -13
- package/src/runtime/admin-setup-ops.ts +145 -36
- package/src/runtime/archive-ops.test.ts +0 -28
- package/src/runtime/branching-ops.test.ts +0 -17
- package/src/runtime/capture-ops.test.ts +41 -16
- package/src/runtime/capture-ops.ts +78 -46
- package/src/runtime/chain-ops.test.ts +0 -21
- package/src/runtime/facades/admin-facade.test.ts +0 -34
- package/src/runtime/facades/agency-facade.test.ts +0 -39
- package/src/runtime/facades/archive-facade.test.ts +0 -43
- package/src/runtime/facades/brain-facade.test.ts +8 -99
- package/src/runtime/facades/brain-facade.ts +29 -12
- package/src/runtime/facades/branching-facade.test.ts +30 -17
- package/src/runtime/facades/chat-facade.test.ts +0 -91
- package/src/runtime/facades/chat-service-ops.test.ts +0 -24
- package/src/runtime/facades/chat-session-ops.test.ts +0 -12
- package/src/runtime/facades/chat-transport-ops.test.ts +0 -23
- package/src/runtime/facades/context-facade.test.ts +0 -17
- package/src/runtime/facades/context-facade.ts +11 -4
- package/src/runtime/facades/control-facade.test.ts +0 -30
- package/src/runtime/facades/curator-facade.test.ts +0 -33
- package/src/runtime/facades/intake-facade.test.ts +0 -33
- package/src/runtime/facades/links-facade.test.ts +0 -37
- package/src/runtime/facades/loop-facade.test.ts +0 -26
- package/src/runtime/facades/memory-facade.test.ts +0 -18
- package/src/runtime/facades/memory-facade.ts +27 -11
- package/src/runtime/facades/operator-facade.test.ts +0 -31
- package/src/runtime/facades/orchestrate-facade.test.ts +0 -21
- package/src/runtime/facades/orchestrate-facade.ts +39 -1
- package/src/runtime/facades/plan-facade.test.ts +7 -32
- package/src/runtime/facades/plan-facade.ts +137 -4
- package/src/runtime/facades/review-facade.test.ts +1 -49
- package/src/runtime/facades/sync-facade.test.ts +24 -41
- package/src/runtime/facades/tier-facade.test.ts +30 -22
- package/src/runtime/facades/vault-facade.test.ts +0 -41
- package/src/runtime/facades/vault-facade.ts +26 -3
- package/src/runtime/grading-ops.test.ts +0 -27
- package/src/runtime/intake-ops.test.ts +0 -19
- package/src/runtime/loop-ops.test.ts +0 -48
- package/src/runtime/memory-cross-project-ops.test.ts +0 -14
- package/src/runtime/memory-extra-ops.test.ts +4 -8
- package/src/runtime/orchestrate-ops.test.ts +238 -19
- package/src/runtime/orchestrate-ops.ts +166 -41
- package/src/runtime/pack-ops.test.ts +0 -26
- package/src/runtime/planning-extra-ops.test.ts +2 -14
- package/src/runtime/playbook-ops-execution.test.ts +9 -20
- package/src/runtime/playbook-ops.test.ts +4 -67
- package/src/runtime/review-ops.test.ts +0 -15
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/schema-helpers.ts +4 -0
- package/src/runtime/sync-ops.test.ts +0 -18
- package/src/runtime/tier-ops.test.ts +0 -21
- package/src/runtime/types.ts +19 -0
- package/src/runtime/vault-extra-ops.test.ts +0 -12
- package/src/runtime/vault-linking-ops.test.ts +0 -4
- package/src/runtime/vault-linking-ops.ts +26 -8
- package/src/runtime/vault-sharing-ops.test.ts +0 -9
- package/src/scheduler/cron-validator.ts +101 -0
- package/src/scheduler/platform-linux.ts +122 -0
- package/src/scheduler/platform-macos.ts +150 -0
- package/src/scheduler/scheduler-ops.ts +77 -0
- package/src/scheduler/scheduler.test.ts +247 -0
- package/src/scheduler/scheduler.ts +174 -0
- package/src/scheduler/types.ts +52 -0
- package/src/skills/__tests__/sync-skills.test.ts +6 -17
- package/src/skills/global-claude-md.test.ts +113 -0
- package/src/skills/sync-skills.ts +143 -35
- package/src/skills/validate-skills.test.ts +206 -0
- package/src/skills/validate-skills.ts +470 -0
- package/src/telemetry/telemetry.test.ts +1 -0
- package/src/transport/http-server.test.ts +3 -0
- package/src/transport/session-manager.test.ts +3 -1
- package/src/transport/token-auth.test.ts +6 -9
- package/src/transport/ws-server.test.ts +10 -2
- package/src/utils/worktree-reaper.ts +113 -0
- package/src/vault/__tests__/vault-characterization.test.ts +0 -108
- package/src/vault/default-canonical-tags.ts +64 -0
- package/src/vault/linking.test.ts +0 -2
- package/src/vault/playbook.test.ts +4 -1
- package/src/vault/scope-detector.test.ts +3 -1
- package/src/vault/scope-detector.ts +42 -4
- package/src/vault/tag-normalizer.test.ts +214 -0
- package/src/vault/tag-normalizer.ts +188 -0
- package/src/vault/vault-connect.test.ts +1 -1
- package/src/vault/vault-entries.ts +3 -1
- package/src/vault/vault.test.ts +23 -8
- package/dist/embeddings/index.d.ts +0 -5
- package/dist/embeddings/index.d.ts.map +0 -1
- package/dist/embeddings/index.js +0 -3
- package/dist/embeddings/index.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { createAdminSetupOps } from './admin-setup-ops.js';
|
|
2
|
+
import { createAdminSetupOps, syncHooksToClaudeSettings } from './admin-setup-ops.js';
|
|
3
3
|
import type { AgentRuntime } from './types.js';
|
|
4
4
|
import type { OpDefinition } from '../facades/types.js';
|
|
5
5
|
|
|
@@ -46,6 +46,11 @@ vi.mock('./claude-md-helpers.js', () => ({
|
|
|
46
46
|
injectEngineRulesBlock: vi.fn((content: string) => content),
|
|
47
47
|
}));
|
|
48
48
|
|
|
49
|
+
vi.mock('../paths.js', () => ({
|
|
50
|
+
agentPlansPath: vi.fn(() => '/mock-home/.soleri/test-agent/plans.json'),
|
|
51
|
+
agentVaultPath: vi.fn(() => '/mock-home/.soleri/test-agent/vault.db'),
|
|
52
|
+
}));
|
|
53
|
+
|
|
49
54
|
vi.mock('../skills/sync-skills.js', () => ({
|
|
50
55
|
discoverSkills: vi.fn(() => [{ name: 'skill-1', path: '/mock/skills/skill-1' }]),
|
|
51
56
|
syncSkillsToClaudeCode: vi.fn(() => ({
|
|
@@ -101,18 +106,6 @@ describe('createAdminSetupOps', () => {
|
|
|
101
106
|
ops = createAdminSetupOps(runtime);
|
|
102
107
|
});
|
|
103
108
|
|
|
104
|
-
it('returns 4 ops', () => {
|
|
105
|
-
expect(ops).toHaveLength(4);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('all ops have required fields', () => {
|
|
109
|
-
for (const op of ops) {
|
|
110
|
-
expect(op.name).toBeTruthy();
|
|
111
|
-
expect(op.handler).toBeDefined();
|
|
112
|
-
expect(['read', 'write', 'admin']).toContain(op.auth);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
109
|
describe('admin_inject_claude_md', () => {
|
|
117
110
|
it('returns error when CLAUDE.md not found and createIfMissing is false', async () => {
|
|
118
111
|
const result = (await findOp(ops, 'admin_inject_claude_md').handler({
|
|
@@ -290,6 +283,190 @@ describe('createAdminSetupOps', () => {
|
|
|
290
283
|
});
|
|
291
284
|
});
|
|
292
285
|
|
|
286
|
+
describe('syncHooksToClaudeSettings', () => {
|
|
287
|
+
it('installs SessionStart, PreCompact, and Stop hooks on fresh settings', () => {
|
|
288
|
+
const result = syncHooksToClaudeSettings('test-agent');
|
|
289
|
+
const written = mockFs['/mock-home/.claude/settings.json'];
|
|
290
|
+
expect(written).toBeDefined();
|
|
291
|
+
const settings = JSON.parse(written);
|
|
292
|
+
expect(settings.hooks.SessionStart).toHaveLength(2);
|
|
293
|
+
expect(settings.hooks.PreCompact).toHaveLength(1);
|
|
294
|
+
expect(settings.hooks.Stop).toHaveLength(1);
|
|
295
|
+
expect(result.installed).toContain('SessionStart');
|
|
296
|
+
expect(result.installed).toContain('PreCompact');
|
|
297
|
+
expect(result.installed).toContain('Stop');
|
|
298
|
+
expect(result.updated).toHaveLength(0);
|
|
299
|
+
expect(result.skipped).toHaveLength(0);
|
|
300
|
+
expect(result.error).toBeUndefined();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('includes the {agentId}-mode skill hook in SessionStart', () => {
|
|
304
|
+
syncHooksToClaudeSettings('test-agent');
|
|
305
|
+
const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
|
|
306
|
+
const commands = settings.hooks.SessionStart.flatMap((g: { hooks: { command?: string }[] }) =>
|
|
307
|
+
g.hooks.map((h) => h.command),
|
|
308
|
+
);
|
|
309
|
+
expect(commands.some((c: string) => c.includes('test-agent-mode skill'))).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('includes the admin_health hook in SessionStart', () => {
|
|
313
|
+
syncHooksToClaudeSettings('test-agent');
|
|
314
|
+
const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
|
|
315
|
+
const commands = settings.hooks.SessionStart.flatMap((g: { hooks: { command?: string }[] }) =>
|
|
316
|
+
g.hooks.map((h) => h.command),
|
|
317
|
+
);
|
|
318
|
+
expect(commands.some((c: string) => c.includes('admin_health'))).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('is idempotent — running twice produces the same output', () => {
|
|
322
|
+
syncHooksToClaudeSettings('test-agent');
|
|
323
|
+
const after1 = mockFs['/mock-home/.claude/settings.json'];
|
|
324
|
+
const result2 = syncHooksToClaudeSettings('test-agent');
|
|
325
|
+
const after2 = mockFs['/mock-home/.claude/settings.json'];
|
|
326
|
+
expect(after1).toBe(after2);
|
|
327
|
+
expect(result2.skipped).toContain('SessionStart');
|
|
328
|
+
expect(result2.installed).toHaveLength(0);
|
|
329
|
+
expect(result2.updated).toHaveLength(0);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('preserves non-agent hooks already in settings', () => {
|
|
333
|
+
mockFs['/mock-home/.claude/settings.json'] = JSON.stringify({
|
|
334
|
+
hooks: {
|
|
335
|
+
SessionStart: [{ hooks: [{ type: 'command', command: 'echo existing' }] }],
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
syncHooksToClaudeSettings('test-agent');
|
|
339
|
+
const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
|
|
340
|
+
const commands = settings.hooks.SessionStart.flatMap((g: { hooks: { command?: string }[] }) =>
|
|
341
|
+
g.hooks.map((h) => h.command),
|
|
342
|
+
);
|
|
343
|
+
expect(commands).toContain('echo existing');
|
|
344
|
+
expect(commands.some((c: string) => c.includes('admin_health'))).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('updates stale agent hooks to match current defaults', () => {
|
|
348
|
+
// A stale hook contains the agent marker but outdated content
|
|
349
|
+
mockFs['/mock-home/.claude/settings.json'] = JSON.stringify({
|
|
350
|
+
hooks: {
|
|
351
|
+
SessionStart: [
|
|
352
|
+
{
|
|
353
|
+
hooks: [
|
|
354
|
+
{
|
|
355
|
+
type: 'command',
|
|
356
|
+
command: `root=$(git rev-parse --show-toplevel 2>/dev/null || echo "."); if grep -q '"test-agent"' "$root/.mcp.json" 2>/dev/null; then echo 'Call mcp__test-agent__test-agent_admin op:OLD_STALE_OP'; fi`,
|
|
357
|
+
timeout: 5000,
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
const result = syncHooksToClaudeSettings('test-agent');
|
|
365
|
+
const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
|
|
366
|
+
const commands = settings.hooks.SessionStart.flatMap((g: { hooks: { command?: string }[] }) =>
|
|
367
|
+
g.hooks.map((h) => h.command),
|
|
368
|
+
);
|
|
369
|
+
expect(commands.some((c: string) => c.includes('OLD_STALE_OP'))).toBe(false);
|
|
370
|
+
expect(commands.some((c: string) => c.includes('admin_health'))).toBe(true);
|
|
371
|
+
expect(result.updated).toContain('SessionStart');
|
|
372
|
+
expect(result.error).toBeUndefined();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('returns error field when write fails', async () => {
|
|
376
|
+
const { writeFileSync } = await import('node:fs');
|
|
377
|
+
vi.mocked(writeFileSync).mockImplementationOnce(() => {
|
|
378
|
+
throw new Error('EACCES: permission denied');
|
|
379
|
+
});
|
|
380
|
+
const result = syncHooksToClaudeSettings('test-agent');
|
|
381
|
+
expect(result.error).toMatch(/EACCES/);
|
|
382
|
+
expect(result.installed).toHaveLength(0);
|
|
383
|
+
expect(result.updated).toHaveLength(0);
|
|
384
|
+
expect(result.skipped).toHaveLength(0);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('multi-agent hook coexistence', () => {
|
|
389
|
+
type HookGroup = { hooks: { command?: string }[] };
|
|
390
|
+
|
|
391
|
+
function getSessionStartCommands(): string[] {
|
|
392
|
+
const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
|
|
393
|
+
return (settings.hooks.SessionStart as HookGroup[]).flatMap((g) =>
|
|
394
|
+
g.hooks.map((h) => h.command ?? ''),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function getAgentCommands(agentId: string): string[] {
|
|
399
|
+
const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
|
|
400
|
+
return (settings.hooks.SessionStart as HookGroup[])
|
|
401
|
+
.flatMap((g) => g.hooks)
|
|
402
|
+
.map((h) => h.command ?? '')
|
|
403
|
+
.filter((c) => c.includes(agentId));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
it('install A then B — both sets present, no overlap', () => {
|
|
407
|
+
syncHooksToClaudeSettings('agent-a');
|
|
408
|
+
syncHooksToClaudeSettings('agent-b');
|
|
409
|
+
|
|
410
|
+
const commands = getSessionStartCommands();
|
|
411
|
+
|
|
412
|
+
// Both agents must have hooks present
|
|
413
|
+
expect(commands.some((c) => c.includes('agent-a'))).toBe(true);
|
|
414
|
+
expect(commands.some((c) => c.includes('agent-b'))).toBe(true);
|
|
415
|
+
|
|
416
|
+
// agent-a commands must not mention agent-b and vice versa
|
|
417
|
+
const aCommands = getAgentCommands('agent-a');
|
|
418
|
+
const bCommands = getAgentCommands('agent-b');
|
|
419
|
+
|
|
420
|
+
expect(aCommands.every((c) => !c.includes('agent-b'))).toBe(true);
|
|
421
|
+
expect(bCommands.every((c) => !c.includes('agent-a'))).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('re-install A after B — B hooks untouched', () => {
|
|
425
|
+
syncHooksToClaudeSettings('agent-a');
|
|
426
|
+
syncHooksToClaudeSettings('agent-b');
|
|
427
|
+
|
|
428
|
+
const beforeB = getAgentCommands('agent-b');
|
|
429
|
+
|
|
430
|
+
syncHooksToClaudeSettings('agent-a'); // re-run A (e.g. after update)
|
|
431
|
+
|
|
432
|
+
const afterB = getAgentCommands('agent-b');
|
|
433
|
+
|
|
434
|
+
expect(afterB).toEqual(beforeB);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('no duplicates after running both twice', () => {
|
|
438
|
+
syncHooksToClaudeSettings('agent-a');
|
|
439
|
+
syncHooksToClaudeSettings('agent-b');
|
|
440
|
+
|
|
441
|
+
const settings1 = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
|
|
442
|
+
const groupCountAfterTwo = (settings1.hooks.SessionStart as HookGroup[]).length;
|
|
443
|
+
|
|
444
|
+
syncHooksToClaudeSettings('agent-a');
|
|
445
|
+
syncHooksToClaudeSettings('agent-b');
|
|
446
|
+
|
|
447
|
+
const settings2 = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
|
|
448
|
+
const groupCountAfterFour = (settings2.hooks.SessionStart as HookGroup[]).length;
|
|
449
|
+
|
|
450
|
+
expect(groupCountAfterFour).toBe(groupCountAfterTwo);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('manually added non-agent hook survives all agent installs', () => {
|
|
454
|
+
// Pre-populate settings with a non-agent hook in SessionStart
|
|
455
|
+
mockFs['/mock-home/.claude/settings.json'] = JSON.stringify({
|
|
456
|
+
hooks: {
|
|
457
|
+
SessionStart: [{ hooks: [{ type: 'command', command: 'echo custom-non-agent-hook' }] }],
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
syncHooksToClaudeSettings('agent-a');
|
|
462
|
+
syncHooksToClaudeSettings('agent-b');
|
|
463
|
+
syncHooksToClaudeSettings('agent-a');
|
|
464
|
+
|
|
465
|
+
const commands = getSessionStartCommands();
|
|
466
|
+
expect(commands).toContain('echo custom-non-agent-hook');
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
293
470
|
describe('admin_check_persistence', () => {
|
|
294
471
|
it('returns NO_STORAGE_DIRECTORY when nothing exists', async () => {
|
|
295
472
|
const result = (await findOp(ops, 'admin_check_persistence').handler({})) as Record<
|
|
@@ -330,5 +507,44 @@ describe('createAdminSetupOps', () => {
|
|
|
330
507
|
expect(activePlans[0].status).toBe('executing');
|
|
331
508
|
expect(result.recommendation).toContain('need attention');
|
|
332
509
|
});
|
|
510
|
+
|
|
511
|
+
it('uses configured or resolved .soleri plan paths and understands planner stores', async () => {
|
|
512
|
+
runtime = {
|
|
513
|
+
...createMockRuntime(),
|
|
514
|
+
config: {
|
|
515
|
+
agentId: 'test-agent',
|
|
516
|
+
dataDir: '/mock/agent-data',
|
|
517
|
+
agentDir: '/mock/agent-dir',
|
|
518
|
+
},
|
|
519
|
+
} as unknown as AgentRuntime;
|
|
520
|
+
ops = createAdminSetupOps(runtime);
|
|
521
|
+
|
|
522
|
+
mockDirs.add('/mock-home/.soleri/test-agent');
|
|
523
|
+
mockFs['/mock-home/.soleri/test-agent/vault.db'] = 'binary';
|
|
524
|
+
mockFs['/mock-home/.soleri/test-agent/plans.json'] = JSON.stringify({
|
|
525
|
+
version: '1.0',
|
|
526
|
+
plans: [
|
|
527
|
+
{ id: 'plan-1', status: 'executing' },
|
|
528
|
+
{ id: 'plan-2', status: 'completed' },
|
|
529
|
+
],
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const result = (await findOp(ops, 'admin_check_persistence').handler({})) as Record<
|
|
533
|
+
string,
|
|
534
|
+
unknown
|
|
535
|
+
>;
|
|
536
|
+
|
|
537
|
+
expect((result.storageDirectory as Record<string, unknown>).path).toBe(
|
|
538
|
+
'/mock-home/.soleri/test-agent',
|
|
539
|
+
);
|
|
540
|
+
expect(
|
|
541
|
+
((result.files as Record<string, unknown>).plans as Record<string, unknown>).path,
|
|
542
|
+
).toBe('/mock-home/.soleri/test-agent/plans.json');
|
|
543
|
+
expect(
|
|
544
|
+
((result.files as Record<string, unknown>).plans as Record<string, unknown>).items,
|
|
545
|
+
).toBe(2);
|
|
546
|
+
expect(result.status).toBe('PERSISTENCE_ACTIVE');
|
|
547
|
+
expect(result.activePlans).toEqual([{ id: 'plan-1', status: 'executing' }]);
|
|
548
|
+
});
|
|
333
549
|
});
|
|
334
550
|
});
|
|
@@ -27,6 +27,10 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
27
27
|
import { homedir } from 'node:os';
|
|
28
28
|
import type { OpDefinition } from '../facades/types.js';
|
|
29
29
|
import type { AgentRuntime } from './types.js';
|
|
30
|
+
import {
|
|
31
|
+
agentPlansPath as getAgentPlansPath,
|
|
32
|
+
agentVaultPath as getAgentVaultPath,
|
|
33
|
+
} from '../paths.js';
|
|
30
34
|
import {
|
|
31
35
|
hasSections,
|
|
32
36
|
removeSections,
|
|
@@ -74,19 +78,63 @@ function getFileInfo(path: string): { exists: boolean; size: number; items: numb
|
|
|
74
78
|
try {
|
|
75
79
|
const stat = statSync(path);
|
|
76
80
|
const content = JSON.parse(readFileSync(path, 'utf-8'));
|
|
77
|
-
const items = content
|
|
78
|
-
? Object.keys(content.items).length
|
|
79
|
-
: content.contexts
|
|
80
|
-
? content.contexts.length
|
|
81
|
-
: Array.isArray(content)
|
|
82
|
-
? content.length
|
|
83
|
-
: 0;
|
|
81
|
+
const items = countPersistedItems(content);
|
|
84
82
|
return { exists: true, size: stat.size, items };
|
|
85
83
|
} catch {
|
|
86
84
|
return { exists: true, size: 0, items: -1 };
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
|
|
88
|
+
function countPersistedItems(content: unknown): number {
|
|
89
|
+
if (Array.isArray(content)) return content.length;
|
|
90
|
+
if (!content || typeof content !== 'object') return 0;
|
|
91
|
+
|
|
92
|
+
const data = content as Record<string, unknown>;
|
|
93
|
+
if (Array.isArray(data.plans)) return data.plans.length;
|
|
94
|
+
if (data.items && typeof data.items === 'object') return Object.keys(data.items).length;
|
|
95
|
+
if (Array.isArray(data.contexts)) return data.contexts.length;
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function extractActivePlans(content: unknown): Array<{ id: string; status: string }> {
|
|
100
|
+
if (!content || typeof content !== 'object') return [];
|
|
101
|
+
|
|
102
|
+
const plans = Array.isArray((content as Record<string, unknown>).plans)
|
|
103
|
+
? ((content as Record<string, unknown>).plans as unknown[])
|
|
104
|
+
: null;
|
|
105
|
+
if (plans) {
|
|
106
|
+
return plans.flatMap((plan) => {
|
|
107
|
+
if (!plan || typeof plan !== 'object') return [];
|
|
108
|
+
const p = plan as Record<string, unknown>;
|
|
109
|
+
const id = typeof p.id === 'string' ? p.id : null;
|
|
110
|
+
const lifecycle =
|
|
111
|
+
typeof p.lifecycleStatus === 'string'
|
|
112
|
+
? p.lifecycleStatus
|
|
113
|
+
: typeof p.status === 'string'
|
|
114
|
+
? p.status
|
|
115
|
+
: null;
|
|
116
|
+
if (!id || (lifecycle !== 'executing' && lifecycle !== 'reconciling')) return [];
|
|
117
|
+
return [{ id, status: lifecycle }];
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const items = (content as Record<string, unknown>).items;
|
|
122
|
+
if (!items || typeof items !== 'object') return [];
|
|
123
|
+
|
|
124
|
+
return Object.entries(items).flatMap(([id, plan]) => {
|
|
125
|
+
if (!plan || typeof plan !== 'object') return [];
|
|
126
|
+
const p = plan as Record<string, unknown>;
|
|
127
|
+
const lifecycle =
|
|
128
|
+
typeof p.lifecycleStatus === 'string'
|
|
129
|
+
? p.lifecycleStatus
|
|
130
|
+
: typeof p.status === 'string'
|
|
131
|
+
? p.status
|
|
132
|
+
: null;
|
|
133
|
+
if (lifecycle !== 'executing' && lifecycle !== 'reconciling') return [];
|
|
134
|
+
return [{ id, status: lifecycle }];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
90
138
|
/** Discover hookify rule files in a directory */
|
|
91
139
|
function discoverHookifyFiles(dir: string): Array<{ name: string; path: string }> {
|
|
92
140
|
if (!existsSync(dir)) return [];
|
|
@@ -97,6 +145,37 @@ function discoverHookifyFiles(dir: string): Array<{ name: string; path: string }
|
|
|
97
145
|
|
|
98
146
|
// discoverSkills imported from '../skills/sync-skills.js'
|
|
99
147
|
|
|
148
|
+
// ─── Deep Equality Helper ─────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/** Recursively compare two values by structure, independent of key insertion order. */
|
|
151
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
152
|
+
if (a === b) return true;
|
|
153
|
+
if (a === null || b === null) return false;
|
|
154
|
+
if (typeof a !== typeof b) return false;
|
|
155
|
+
if (typeof a !== 'object') return false;
|
|
156
|
+
|
|
157
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
158
|
+
|
|
159
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
160
|
+
if (a.length !== b.length) return false;
|
|
161
|
+
for (let i = 0; i < a.length; i++) {
|
|
162
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const objA = a as Record<string, unknown>;
|
|
168
|
+
const objB = b as Record<string, unknown>;
|
|
169
|
+
const keysA = Object.keys(objA);
|
|
170
|
+
const keysB = Object.keys(objB);
|
|
171
|
+
if (keysA.length !== keysB.length) return false;
|
|
172
|
+
for (const key of keysA) {
|
|
173
|
+
if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
|
|
174
|
+
if (!deepEqual(objA[key], objB[key])) return false;
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
100
179
|
// ─── Settings.json Hook Merging ───────────────────────────────────────
|
|
101
180
|
|
|
102
181
|
interface SettingsHook {
|
|
@@ -119,7 +198,7 @@ interface SettingsHookGroup {
|
|
|
119
198
|
function buildConditionalHookCommand(agentId: string, instruction: string): string {
|
|
120
199
|
// Escape single quotes in instruction for safe shell embedding
|
|
121
200
|
const escaped = instruction.replace(/'/g, "'\\''");
|
|
122
|
-
return `root=$(git rev-parse --show-toplevel 2>/dev/null || echo "."); if grep -
|
|
201
|
+
return `root=$(git rev-parse --show-toplevel 2>/dev/null || echo "."); if grep -qF '"${agentId}"' "$root/.mcp.json" 2>/dev/null; then echo '${escaped}'; fi`;
|
|
123
202
|
}
|
|
124
203
|
|
|
125
204
|
/** Default lifecycle hooks for any Soleri agent */
|
|
@@ -141,6 +220,16 @@ function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookG
|
|
|
141
220
|
},
|
|
142
221
|
],
|
|
143
222
|
},
|
|
223
|
+
{
|
|
224
|
+
matcher: '',
|
|
225
|
+
hooks: [
|
|
226
|
+
{
|
|
227
|
+
type: 'command',
|
|
228
|
+
command: `echo 'SESSION_START: Invoke the ${agentId}-mode skill now to load full routing context and command reference.'`,
|
|
229
|
+
timeout: 5,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
144
233
|
],
|
|
145
234
|
PreCompact: [
|
|
146
235
|
{
|
|
@@ -178,8 +267,11 @@ function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookG
|
|
|
178
267
|
/** Check if a hook group belongs to this agent by inspecting prompts for the marker */
|
|
179
268
|
function isAgentHookGroup(group: SettingsHookGroup, agentId: string): boolean {
|
|
180
269
|
const marker = `mcp__${agentId}__${agentId}_`;
|
|
270
|
+
const skillMarker = `${agentId}-mode skill`;
|
|
181
271
|
return group.hooks.some(
|
|
182
|
-
(h) =>
|
|
272
|
+
(h) =>
|
|
273
|
+
(h.prompt && (h.prompt.includes(marker) || h.prompt.includes(skillMarker))) ||
|
|
274
|
+
(h.command && (h.command.includes(marker) || h.command.includes(skillMarker))),
|
|
183
275
|
);
|
|
184
276
|
}
|
|
185
277
|
|
|
@@ -206,29 +298,55 @@ function mergeSettingsHooks(
|
|
|
206
298
|
continue;
|
|
207
299
|
}
|
|
208
300
|
|
|
209
|
-
//
|
|
210
|
-
const
|
|
301
|
+
// Remove all existing agent-owned groups, keep non-agent hooks
|
|
302
|
+
const nonAgentGroups = merged[event].filter((g) => !isAgentHookGroup(g, agentId));
|
|
303
|
+
const existingAgentGroups = merged[event].filter((g) => isAgentHookGroup(g, agentId));
|
|
211
304
|
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
305
|
+
if (deepEqual(existingAgentGroups, groups)) {
|
|
306
|
+
skipped.push(event);
|
|
307
|
+
} else if (existingAgentGroups.length === 0) {
|
|
308
|
+
merged[event] = [...nonAgentGroups, ...groups];
|
|
215
309
|
installed.push(event);
|
|
216
310
|
} else {
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (existing === template) {
|
|
221
|
-
skipped.push(event);
|
|
222
|
-
} else {
|
|
223
|
-
merged[event][existingIdx] = groups[0];
|
|
224
|
-
updated.push(event);
|
|
225
|
-
}
|
|
311
|
+
// Replace all agent groups with current defaults
|
|
312
|
+
merged[event] = [...nonAgentGroups, ...groups];
|
|
313
|
+
updated.push(event);
|
|
226
314
|
}
|
|
227
315
|
}
|
|
228
316
|
|
|
229
317
|
return { hooks: merged, installed, updated, skipped };
|
|
230
318
|
}
|
|
231
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Auto-sync lifecycle hooks into ~/.claude/settings.json at engine startup.
|
|
322
|
+
* Idempotent — skips hooks already present, updates stale ones.
|
|
323
|
+
* Returns a summary of what was installed, updated, or skipped.
|
|
324
|
+
*/
|
|
325
|
+
export function syncHooksToClaudeSettings(agentId: string): {
|
|
326
|
+
installed: string[];
|
|
327
|
+
updated: string[];
|
|
328
|
+
skipped: string[];
|
|
329
|
+
error?: string;
|
|
330
|
+
} {
|
|
331
|
+
try {
|
|
332
|
+
const settings = readSettingsJson();
|
|
333
|
+
const currentHooks = (settings.hooks ?? {}) as Record<string, SettingsHookGroup[]>;
|
|
334
|
+
const { hooks, installed, updated, skipped } = mergeSettingsHooks(currentHooks, agentId);
|
|
335
|
+
if (installed.length > 0 || updated.length > 0) {
|
|
336
|
+
writeSettingsJson({ ...settings, hooks });
|
|
337
|
+
}
|
|
338
|
+
return { installed, updated, skipped };
|
|
339
|
+
} catch (err) {
|
|
340
|
+
// Non-fatal — hooks will be installed on next run or via admin_setup_global
|
|
341
|
+
return {
|
|
342
|
+
installed: [],
|
|
343
|
+
updated: [],
|
|
344
|
+
skipped: [],
|
|
345
|
+
error: err instanceof Error ? err.message : String(err),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
232
350
|
// ─── Op Definitions ───────────────────────────────────────────────────
|
|
233
351
|
|
|
234
352
|
/**
|
|
@@ -621,15 +739,15 @@ export function createAdminSetupOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
621
739
|
auth: 'read',
|
|
622
740
|
handler: async () => {
|
|
623
741
|
const { agentId, plansPath, vaultPath } = config;
|
|
624
|
-
const
|
|
742
|
+
const plansFile = plansPath ?? getAgentPlansPath(agentId);
|
|
743
|
+
const vaultFile = vaultPath ?? getAgentVaultPath(agentId);
|
|
744
|
+
const storageDir = dirname(plansFile);
|
|
625
745
|
const storageDirExists = existsSync(storageDir);
|
|
626
746
|
|
|
627
747
|
// Check plan storage
|
|
628
|
-
const plansFile = plansPath ?? join(storageDir, 'plans.json');
|
|
629
748
|
const plansInfo = getFileInfo(plansFile);
|
|
630
749
|
|
|
631
750
|
// Check vault
|
|
632
|
-
const vaultFile = vaultPath ?? join(storageDir, 'vault.db');
|
|
633
751
|
const vaultExists = existsSync(vaultFile);
|
|
634
752
|
let vaultSize = 0;
|
|
635
753
|
if (vaultExists) {
|
|
@@ -655,16 +773,7 @@ export function createAdminSetupOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
655
773
|
if (plansInfo.exists) {
|
|
656
774
|
try {
|
|
657
775
|
const plansData = JSON.parse(readFileSync(plansFile, 'utf-8'));
|
|
658
|
-
|
|
659
|
-
if (typeof items === 'object' && items !== null) {
|
|
660
|
-
for (const [id, plan] of Object.entries(items)) {
|
|
661
|
-
const p = plan as Record<string, unknown>;
|
|
662
|
-
const lifecycle = (p.lifecycleStatus ?? p.status) as string | undefined;
|
|
663
|
-
if (lifecycle === 'executing' || lifecycle === 'reconciling') {
|
|
664
|
-
activePlans.push({ id, status: lifecycle });
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
776
|
+
activePlans.push(...extractActivePlans(plansData));
|
|
668
777
|
} catch {
|
|
669
778
|
// Parse error — not critical
|
|
670
779
|
}
|
|
@@ -83,34 +83,6 @@ describe('createArchiveOps', () => {
|
|
|
83
83
|
ops = createArchiveOps(runtime);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
it('returns 12 ops', () => {
|
|
87
|
-
expect(ops).toHaveLength(12);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('all ops have required fields', () => {
|
|
91
|
-
for (const op of ops) {
|
|
92
|
-
expect(op.name).toBeTruthy();
|
|
93
|
-
expect(op.handler).toBeDefined();
|
|
94
|
-
expect(['read', 'write', 'admin']).toContain(op.auth);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('contains expected op names', () => {
|
|
99
|
-
const names = ops.map((o) => o.name);
|
|
100
|
-
expect(names).toContain('vault_archive');
|
|
101
|
-
expect(names).toContain('vault_restore');
|
|
102
|
-
expect(names).toContain('vault_optimize');
|
|
103
|
-
expect(names).toContain('vault_backup');
|
|
104
|
-
expect(names).toContain('vault_age_report');
|
|
105
|
-
expect(names).toContain('vault_set_temporal');
|
|
106
|
-
expect(names).toContain('vault_find_expiring');
|
|
107
|
-
expect(names).toContain('vault_find_expired');
|
|
108
|
-
expect(names).toContain('knowledge_audit');
|
|
109
|
-
expect(names).toContain('knowledge_health');
|
|
110
|
-
expect(names).toContain('knowledge_merge');
|
|
111
|
-
expect(names).toContain('knowledge_reorganize');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
86
|
describe('vault_archive', () => {
|
|
115
87
|
it('archives old entries', async () => {
|
|
116
88
|
await findOp(ops, 'vault_archive').handler({
|
|
@@ -35,23 +35,6 @@ describe('branching-ops', () => {
|
|
|
35
35
|
ops = captureOps(createBranchingOps(runtime));
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
it('registers all 5 branching ops', () => {
|
|
39
|
-
expect(ops.size).toBe(5);
|
|
40
|
-
expect(ops.has('vault_branch')).toBe(true);
|
|
41
|
-
expect(ops.has('vault_branch_add')).toBe(true);
|
|
42
|
-
expect(ops.has('vault_branch_list')).toBe(true);
|
|
43
|
-
expect(ops.has('vault_merge_branch')).toBe(true);
|
|
44
|
-
expect(ops.has('vault_delete_branch')).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('has correct auth levels', () => {
|
|
48
|
-
expect(ops.get('vault_branch')!.auth).toBe('write');
|
|
49
|
-
expect(ops.get('vault_branch_add')!.auth).toBe('write');
|
|
50
|
-
expect(ops.get('vault_branch_list')!.auth).toBe('read');
|
|
51
|
-
expect(ops.get('vault_merge_branch')!.auth).toBe('admin');
|
|
52
|
-
expect(ops.get('vault_delete_branch')!.auth).toBe('admin');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
38
|
describe('vault_branch', () => {
|
|
56
39
|
it('creates a branch', async () => {
|
|
57
40
|
const result = await executeOp(ops, 'vault_branch', { name: 'experiment' });
|
|
@@ -83,18 +83,6 @@ describe('createCaptureOps', () => {
|
|
|
83
83
|
ops = createCaptureOps(runtime);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
it('returns 4 ops', () => {
|
|
87
|
-
expect(ops).toHaveLength(4);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('all ops have required fields', () => {
|
|
91
|
-
for (const op of ops) {
|
|
92
|
-
expect(op.name).toBeTruthy();
|
|
93
|
-
expect(op.handler).toBeDefined();
|
|
94
|
-
expect(['read', 'write']).toContain(op.auth);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
86
|
describe('capture_knowledge', () => {
|
|
99
87
|
it('captures a single entry with governance approval', async () => {
|
|
100
88
|
const result = (await findOp(ops, 'capture_knowledge').handler({
|
|
@@ -191,7 +179,7 @@ describe('createCaptureOps', () => {
|
|
|
191
179
|
entries: [{ type: 'pattern', domain: 'a', title: 'A', description: 'a', tags: [] }],
|
|
192
180
|
})) as Record<string, unknown>;
|
|
193
181
|
expect(result.autoLinkedCount).toBe(1);
|
|
194
|
-
expect(result.suggestedLinks).
|
|
182
|
+
expect((result.suggestedLinks as unknown[]).length).toBe(1); // one suggestion returned by mock
|
|
195
183
|
});
|
|
196
184
|
|
|
197
185
|
it('maps extended types correctly', async () => {
|
|
@@ -252,7 +240,8 @@ describe('createCaptureOps', () => {
|
|
|
252
240
|
description: 'A quick capture',
|
|
253
241
|
})) as Record<string, unknown>;
|
|
254
242
|
expect(result.captured).toBe(true);
|
|
255
|
-
expect(result.id).
|
|
243
|
+
expect(typeof result.id).toBe('string');
|
|
244
|
+
expect((result.id as string).startsWith('testing-')).toBe(true); // id is generated as <domain>-<timestamp>-<random>
|
|
256
245
|
expect((result.governance as Record<string, unknown>).action).toBe('capture');
|
|
257
246
|
});
|
|
258
247
|
|
|
@@ -380,6 +369,43 @@ describe('createCaptureOps', () => {
|
|
|
380
369
|
// Capture should still succeed despite sync error
|
|
381
370
|
expect(result.captured).toBe(true);
|
|
382
371
|
});
|
|
372
|
+
|
|
373
|
+
it('adds planningNote when type is anti-pattern', async () => {
|
|
374
|
+
const result = (await findOp(ops, 'capture_quick').handler({
|
|
375
|
+
type: 'anti-pattern',
|
|
376
|
+
domain: 'testing',
|
|
377
|
+
title: 'No content body',
|
|
378
|
+
description: 'captured without context/example/why',
|
|
379
|
+
})) as Record<string, unknown>;
|
|
380
|
+
expect(result.captured).toBe(true);
|
|
381
|
+
expect(typeof result.planningNote).toBe('string');
|
|
382
|
+
expect(result.planningNote as string).toContain('capture_knowledge');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('adds planningNote when tags include planning-gate', async () => {
|
|
386
|
+
const result = (await findOp(ops, 'capture_quick').handler({
|
|
387
|
+
type: 'rule',
|
|
388
|
+
domain: 'testing',
|
|
389
|
+
title: 'Gated rule',
|
|
390
|
+
description: 'should warn',
|
|
391
|
+
tags: ['planning-gate'],
|
|
392
|
+
})) as Record<string, unknown>;
|
|
393
|
+
expect(result.captured).toBe(true);
|
|
394
|
+
expect(typeof result.planningNote).toBe('string');
|
|
395
|
+
expect(result.planningNote as string).toContain('capture_knowledge');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('does not add planningNote for non-planning types without planning tags', async () => {
|
|
399
|
+
const result = (await findOp(ops, 'capture_quick').handler({
|
|
400
|
+
type: 'reference',
|
|
401
|
+
domain: 'docs',
|
|
402
|
+
title: 'A plain reference',
|
|
403
|
+
description: 'no planning intent',
|
|
404
|
+
tags: ['docs'],
|
|
405
|
+
})) as Record<string, unknown>;
|
|
406
|
+
expect(result.captured).toBe(true);
|
|
407
|
+
expect(result.planningNote).toBeUndefined();
|
|
408
|
+
});
|
|
383
409
|
});
|
|
384
410
|
|
|
385
411
|
describe('search_intelligent', () => {
|
|
@@ -387,8 +413,7 @@ describe('createCaptureOps', () => {
|
|
|
387
413
|
const result = (await findOp(ops, 'search_intelligent').handler({
|
|
388
414
|
query: 'auth patterns',
|
|
389
415
|
})) as Array<Record<string, unknown>>;
|
|
390
|
-
expect(
|
|
391
|
-
expect(result.length).toBeGreaterThan(0);
|
|
416
|
+
expect(result).toHaveLength(1); // brain.intelligentSearch mock returns exactly 1 result
|
|
392
417
|
expect(result[0].source).toBe('vault');
|
|
393
418
|
});
|
|
394
419
|
|