@soleri/core 9.15.0 → 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/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/context/context-engine.js +1 -1
- package/dist/context/context-engine.js.map +1 -1
- 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 +31 -4
- 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 +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.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 +1 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +7 -0
- 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/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 +87 -17
- 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 +12 -0
- 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/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/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/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/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/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 +1 -1
- 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/second-brain-features.test.ts +2 -14
- 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/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/tag-manager.test.ts +0 -4
- 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 +17 -0
- package/src/intake/dedup-gate.test.ts +2 -6
- package/src/intake/text-ingester.test.ts +3 -4
- 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.ts +8 -0
- 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 +185 -13
- package/src/runtime/admin-setup-ops.ts +86 -16
- 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 +12 -0
- 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/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/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 +12 -11
- 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/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/vault-connect.test.ts +1 -1
- package/src/vault/vault-entries.ts +3 -1
- package/src/vault/vault.test.ts +23 -8
package/src/runtime/admin-ops.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* runtime state. No new modules needed — uses existing runtime parts.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
8
|
+
import { readFileSync, statSync, existsSync, readdirSync } from 'node:fs';
|
|
9
9
|
import { join, dirname } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import type { OpDefinition } from '../facades/types.js';
|
|
12
13
|
import type { AgentRuntime } from './types.js';
|
|
@@ -147,20 +148,21 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
147
148
|
};
|
|
148
149
|
}
|
|
149
150
|
// Fallback — just describe admin ops
|
|
151
|
+
const adminOps = [
|
|
152
|
+
'admin_health',
|
|
153
|
+
'admin_tool_list',
|
|
154
|
+
'admin_config',
|
|
155
|
+
'admin_vault_size',
|
|
156
|
+
'admin_uptime',
|
|
157
|
+
'admin_version',
|
|
158
|
+
'admin_reset_cache',
|
|
159
|
+
'admin_diagnostic',
|
|
160
|
+
];
|
|
150
161
|
return {
|
|
151
|
-
count:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'admin_tool_list',
|
|
156
|
-
'admin_config',
|
|
157
|
-
'admin_vault_size',
|
|
158
|
-
'admin_uptime',
|
|
159
|
-
'admin_version',
|
|
160
|
-
'admin_reset_cache',
|
|
161
|
-
'admin_diagnostic',
|
|
162
|
-
],
|
|
163
|
-
},
|
|
162
|
+
count: adminOps.length,
|
|
163
|
+
scope: 'admin-only',
|
|
164
|
+
note: 'Pass _allOps for full system op count',
|
|
165
|
+
ops: { admin: adminOps },
|
|
164
166
|
routing: buildRoutingHints(),
|
|
165
167
|
};
|
|
166
168
|
},
|
|
@@ -380,7 +382,7 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
380
382
|
});
|
|
381
383
|
}
|
|
382
384
|
|
|
383
|
-
// 7. Skills
|
|
385
|
+
// 7. Skills — check discovered vs registered in .claude/skills/
|
|
384
386
|
try {
|
|
385
387
|
const agentDir = runtime.config.agentDir;
|
|
386
388
|
const skillsDirs = agentDir ? [join(agentDir, 'skills')] : [];
|
|
@@ -388,12 +390,52 @@ export function createAdminOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
388
390
|
const installedPacks = packInstaller.list();
|
|
389
391
|
const packSkillCount = installedPacks.reduce((sum, p) => sum + p.skills.length, 0);
|
|
390
392
|
const totalSkills = agentSkills.length + packSkillCount;
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
393
|
+
|
|
394
|
+
// Check registration status in .claude/skills/
|
|
395
|
+
const claudeSkillsDir = join(homedir(), '.claude', 'skills');
|
|
396
|
+
let registeredCount = 0;
|
|
397
|
+
let brokenCount = 0;
|
|
398
|
+
const unregistered: string[] = [];
|
|
399
|
+
|
|
400
|
+
if (existsSync(claudeSkillsDir)) {
|
|
401
|
+
try {
|
|
402
|
+
const registered = readdirSync(claudeSkillsDir, { withFileTypes: true });
|
|
403
|
+
registeredCount = registered.length;
|
|
404
|
+
for (const entry of registered) {
|
|
405
|
+
if (entry.isSymbolicLink()) {
|
|
406
|
+
try {
|
|
407
|
+
statSync(join(claudeSkillsDir, entry.name));
|
|
408
|
+
} catch {
|
|
409
|
+
brokenCount++;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
// Can't read .claude/skills/ — skip registration check
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (const skill of agentSkills) {
|
|
419
|
+
const skillRegisteredDir = join(claudeSkillsDir, skill.name);
|
|
420
|
+
if (!existsSync(skillRegisteredDir)) {
|
|
421
|
+
unregistered.push(skill.name);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const hasIssues = unregistered.length > 0 || brokenCount > 0;
|
|
426
|
+
// Warn only when agentDir is set but no skills exist anywhere (local OR global)
|
|
427
|
+
const hasAnySkills = totalSkills > 0 || registeredCount > 0;
|
|
428
|
+
const skillStatus = !hasAnySkills && agentDir ? 'warn' : hasIssues ? 'warn' : 'ok';
|
|
429
|
+
const detail = [
|
|
430
|
+
`${totalSkills} discovered (${agentSkills.length} agent, ${packSkillCount} pack)`,
|
|
431
|
+
`${registeredCount} registered in .claude/skills/`,
|
|
432
|
+
...(unregistered.length > 0
|
|
433
|
+
? [`${unregistered.length} unregistered: ${unregistered.join(', ')}`]
|
|
434
|
+
: []),
|
|
435
|
+
...(brokenCount > 0 ? [`${brokenCount} broken links`] : []),
|
|
436
|
+
].join(' — ');
|
|
437
|
+
|
|
438
|
+
checks.push({ name: 'skills', status: skillStatus, detail });
|
|
397
439
|
} catch (err) {
|
|
398
440
|
checks.push({
|
|
399
441
|
name: 'skills',
|
|
@@ -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
|
|
|
@@ -106,18 +106,6 @@ describe('createAdminSetupOps', () => {
|
|
|
106
106
|
ops = createAdminSetupOps(runtime);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
it('returns 4 ops', () => {
|
|
110
|
-
expect(ops).toHaveLength(4);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('all ops have required fields', () => {
|
|
114
|
-
for (const op of ops) {
|
|
115
|
-
expect(op.name).toBeTruthy();
|
|
116
|
-
expect(op.handler).toBeDefined();
|
|
117
|
-
expect(['read', 'write', 'admin']).toContain(op.auth);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
109
|
describe('admin_inject_claude_md', () => {
|
|
122
110
|
it('returns error when CLAUDE.md not found and createIfMissing is false', async () => {
|
|
123
111
|
const result = (await findOp(ops, 'admin_inject_claude_md').handler({
|
|
@@ -295,6 +283,190 @@ describe('createAdminSetupOps', () => {
|
|
|
295
283
|
});
|
|
296
284
|
});
|
|
297
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
|
+
|
|
298
470
|
describe('admin_check_persistence', () => {
|
|
299
471
|
it('returns NO_STORAGE_DIRECTORY when nothing exists', async () => {
|
|
300
472
|
const result = (await findOp(ops, 'admin_check_persistence').handler({})) as Record<
|
|
@@ -145,6 +145,37 @@ function discoverHookifyFiles(dir: string): Array<{ name: string; path: string }
|
|
|
145
145
|
|
|
146
146
|
// discoverSkills imported from '../skills/sync-skills.js'
|
|
147
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
|
+
|
|
148
179
|
// ─── Settings.json Hook Merging ───────────────────────────────────────
|
|
149
180
|
|
|
150
181
|
interface SettingsHook {
|
|
@@ -167,7 +198,7 @@ interface SettingsHookGroup {
|
|
|
167
198
|
function buildConditionalHookCommand(agentId: string, instruction: string): string {
|
|
168
199
|
// Escape single quotes in instruction for safe shell embedding
|
|
169
200
|
const escaped = instruction.replace(/'/g, "'\\''");
|
|
170
|
-
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`;
|
|
171
202
|
}
|
|
172
203
|
|
|
173
204
|
/** Default lifecycle hooks for any Soleri agent */
|
|
@@ -189,6 +220,16 @@ function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookG
|
|
|
189
220
|
},
|
|
190
221
|
],
|
|
191
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
|
+
},
|
|
192
233
|
],
|
|
193
234
|
PreCompact: [
|
|
194
235
|
{
|
|
@@ -226,8 +267,11 @@ function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookG
|
|
|
226
267
|
/** Check if a hook group belongs to this agent by inspecting prompts for the marker */
|
|
227
268
|
function isAgentHookGroup(group: SettingsHookGroup, agentId: string): boolean {
|
|
228
269
|
const marker = `mcp__${agentId}__${agentId}_`;
|
|
270
|
+
const skillMarker = `${agentId}-mode skill`;
|
|
229
271
|
return group.hooks.some(
|
|
230
|
-
(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))),
|
|
231
275
|
);
|
|
232
276
|
}
|
|
233
277
|
|
|
@@ -254,29 +298,55 @@ function mergeSettingsHooks(
|
|
|
254
298
|
continue;
|
|
255
299
|
}
|
|
256
300
|
|
|
257
|
-
//
|
|
258
|
-
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));
|
|
259
304
|
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
305
|
+
if (deepEqual(existingAgentGroups, groups)) {
|
|
306
|
+
skipped.push(event);
|
|
307
|
+
} else if (existingAgentGroups.length === 0) {
|
|
308
|
+
merged[event] = [...nonAgentGroups, ...groups];
|
|
263
309
|
installed.push(event);
|
|
264
310
|
} else {
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (existing === template) {
|
|
269
|
-
skipped.push(event);
|
|
270
|
-
} else {
|
|
271
|
-
merged[event][existingIdx] = groups[0];
|
|
272
|
-
updated.push(event);
|
|
273
|
-
}
|
|
311
|
+
// Replace all agent groups with current defaults
|
|
312
|
+
merged[event] = [...nonAgentGroups, ...groups];
|
|
313
|
+
updated.push(event);
|
|
274
314
|
}
|
|
275
315
|
}
|
|
276
316
|
|
|
277
317
|
return { hooks: merged, installed, updated, skipped };
|
|
278
318
|
}
|
|
279
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
|
+
|
|
280
350
|
// ─── Op Definitions ───────────────────────────────────────────────────
|
|
281
351
|
|
|
282
352
|
/**
|
|
@@ -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
|
|