hammoc 1.3.0 → 1.5.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/README.md +422 -403
- package/bin/hammoc.js +0 -6
- package/package.json +100 -93
- package/packages/client/dist/assets/agentExampleHighlight-BgwTm15v.js +1 -0
- package/packages/client/dist/assets/commandTokenHighlight-BljHwnrK.js +1 -0
- package/packages/client/dist/assets/index-CjyjnXB8.css +32 -0
- package/packages/client/dist/assets/index-D3LxqW3f.js +2 -0
- package/packages/client/dist/assets/index-NqJdhlek.js +1498 -0
- package/packages/client/dist/assets/snippetTokenHighlight-DWsaQXX0.js +1 -0
- package/packages/client/dist/index.html +2 -2
- package/packages/client/dist/sw.js +1 -1
- package/packages/server/dist/app.d.ts.map +1 -1
- package/packages/server/dist/app.js +24 -24
- package/packages/server/dist/app.js.map +1 -1
- package/packages/server/dist/controllers/boardController.d.ts.map +1 -1
- package/packages/server/dist/controllers/boardController.js +0 -5
- package/packages/server/dist/controllers/boardController.js.map +1 -1
- package/packages/server/dist/controllers/claudeMdController.d.ts +26 -0
- package/packages/server/dist/controllers/claudeMdController.d.ts.map +1 -0
- package/packages/server/dist/controllers/claudeMdController.js +158 -0
- package/packages/server/dist/controllers/claudeMdController.js.map +1 -0
- package/packages/server/dist/controllers/fileSystemController.d.ts +4 -0
- package/packages/server/dist/controllers/fileSystemController.d.ts.map +1 -1
- package/packages/server/dist/controllers/fileSystemController.js +20 -2
- package/packages/server/dist/controllers/fileSystemController.js.map +1 -1
- package/packages/server/dist/controllers/harnessAgentController.d.ts +28 -0
- package/packages/server/dist/controllers/harnessAgentController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessAgentController.js +339 -0
- package/packages/server/dist/controllers/harnessAgentController.js.map +1 -0
- package/packages/server/dist/controllers/harnessCommandController.d.ts +28 -0
- package/packages/server/dist/controllers/harnessCommandController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessCommandController.js +382 -0
- package/packages/server/dist/controllers/harnessCommandController.js.map +1 -0
- package/packages/server/dist/controllers/harnessController.d.ts +21 -0
- package/packages/server/dist/controllers/harnessController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessController.js +176 -0
- package/packages/server/dist/controllers/harnessController.js.map +1 -0
- package/packages/server/dist/controllers/harnessHookController.d.ts +32 -0
- package/packages/server/dist/controllers/harnessHookController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessHookController.js +363 -0
- package/packages/server/dist/controllers/harnessHookController.js.map +1 -0
- package/packages/server/dist/controllers/harnessLintController.d.ts +18 -0
- package/packages/server/dist/controllers/harnessLintController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessLintController.js +72 -0
- package/packages/server/dist/controllers/harnessLintController.js.map +1 -0
- package/packages/server/dist/controllers/harnessMcpController.d.ts +28 -0
- package/packages/server/dist/controllers/harnessMcpController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessMcpController.js +310 -0
- package/packages/server/dist/controllers/harnessMcpController.js.map +1 -0
- package/packages/server/dist/controllers/harnessPluginController.d.ts +17 -0
- package/packages/server/dist/controllers/harnessPluginController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessPluginController.js +115 -0
- package/packages/server/dist/controllers/harnessPluginController.js.map +1 -0
- package/packages/server/dist/controllers/harnessShareScopeController.d.ts +15 -0
- package/packages/server/dist/controllers/harnessShareScopeController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessShareScopeController.js +73 -0
- package/packages/server/dist/controllers/harnessShareScopeController.js.map +1 -0
- package/packages/server/dist/controllers/harnessSkillController.d.ts +32 -0
- package/packages/server/dist/controllers/harnessSkillController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessSkillController.js +453 -0
- package/packages/server/dist/controllers/harnessSkillController.js.map +1 -0
- package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
- package/packages/server/dist/controllers/projectController.js +11 -0
- package/packages/server/dist/controllers/projectController.js.map +1 -1
- package/packages/server/dist/controllers/serverController.d.ts.map +1 -1
- package/packages/server/dist/controllers/serverController.js +84 -49
- package/packages/server/dist/controllers/serverController.js.map +1 -1
- package/packages/server/dist/controllers/snippetController.d.ts +35 -0
- package/packages/server/dist/controllers/snippetController.d.ts.map +1 -0
- package/packages/server/dist/controllers/snippetController.js +294 -0
- package/packages/server/dist/controllers/snippetController.js.map +1 -0
- package/packages/server/dist/handlers/websocket.d.ts +16 -0
- package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
- package/packages/server/dist/handlers/websocket.js +221 -8
- package/packages/server/dist/handlers/websocket.js.map +1 -1
- package/packages/server/dist/index.js +66 -0
- package/packages/server/dist/index.js.map +1 -1
- package/packages/server/dist/locales/en/server.json +41 -6
- package/packages/server/dist/locales/es/server.json +3 -5
- package/packages/server/dist/locales/ja/server.json +3 -5
- package/packages/server/dist/locales/ko/server.json +4 -6
- package/packages/server/dist/locales/pt/server.json +3 -5
- package/packages/server/dist/locales/zh-CN/server.json +3 -5
- package/packages/server/dist/routes/account.d.ts +7 -0
- package/packages/server/dist/routes/account.d.ts.map +1 -0
- package/packages/server/dist/routes/account.js +35 -0
- package/packages/server/dist/routes/account.js.map +1 -0
- package/packages/server/dist/routes/debug.d.ts +1 -1
- package/packages/server/dist/routes/debug.d.ts.map +1 -1
- package/packages/server/dist/routes/debug.js +60 -1
- package/packages/server/dist/routes/debug.js.map +1 -1
- package/packages/server/dist/routes/harness.d.ts +8 -0
- package/packages/server/dist/routes/harness.d.ts.map +1 -0
- package/packages/server/dist/routes/harness.js +92 -0
- package/packages/server/dist/routes/harness.js.map +1 -0
- package/packages/server/dist/routes/preferences.d.ts.map +1 -1
- package/packages/server/dist/routes/preferences.js +11 -2
- package/packages/server/dist/routes/preferences.js.map +1 -1
- package/packages/server/dist/routes/projects.d.ts.map +1 -1
- package/packages/server/dist/routes/projects.js +5 -60
- package/packages/server/dist/routes/projects.js.map +1 -1
- package/packages/server/dist/routes/snippets.d.ts +14 -0
- package/packages/server/dist/routes/snippets.d.ts.map +1 -0
- package/packages/server/dist/routes/snippets.js +27 -0
- package/packages/server/dist/routes/snippets.js.map +1 -0
- package/packages/server/dist/services/accountInfoService.d.ts +38 -0
- package/packages/server/dist/services/accountInfoService.d.ts.map +1 -0
- package/packages/server/dist/services/accountInfoService.js +118 -0
- package/packages/server/dist/services/accountInfoService.js.map +1 -0
- package/packages/server/dist/services/bmadStatusService.d.ts +6 -2
- package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
- package/packages/server/dist/services/bmadStatusService.js +88 -32
- package/packages/server/dist/services/bmadStatusService.js.map +1 -1
- package/packages/server/dist/services/chatService.d.ts +3 -0
- package/packages/server/dist/services/chatService.d.ts.map +1 -1
- package/packages/server/dist/services/chatService.js +36 -8
- package/packages/server/dist/services/chatService.js.map +1 -1
- package/packages/server/dist/services/claudeMdService.d.ts +48 -0
- package/packages/server/dist/services/claudeMdService.d.ts.map +1 -0
- package/packages/server/dist/services/claudeMdService.js +240 -0
- package/packages/server/dist/services/claudeMdService.js.map +1 -0
- package/packages/server/dist/services/commandService.d.ts +10 -0
- package/packages/server/dist/services/commandService.d.ts.map +1 -1
- package/packages/server/dist/services/commandService.js +129 -4
- package/packages/server/dist/services/commandService.js.map +1 -1
- package/packages/server/dist/services/fileSystemService.d.ts +7 -1
- package/packages/server/dist/services/fileSystemService.d.ts.map +1 -1
- package/packages/server/dist/services/fileSystemService.js +67 -8
- package/packages/server/dist/services/fileSystemService.js.map +1 -1
- package/packages/server/dist/services/fileWatcherService.d.ts +59 -0
- package/packages/server/dist/services/fileWatcherService.d.ts.map +1 -0
- package/packages/server/dist/services/fileWatcherService.js +329 -0
- package/packages/server/dist/services/fileWatcherService.js.map +1 -0
- package/packages/server/dist/services/gitService.d.ts.map +1 -1
- package/packages/server/dist/services/gitService.js +67 -7
- package/packages/server/dist/services/gitService.js.map +1 -1
- package/packages/server/dist/services/harnessAgentService.d.ts +79 -0
- package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessAgentService.js +933 -0
- package/packages/server/dist/services/harnessAgentService.js.map +1 -0
- package/packages/server/dist/services/harnessCommandService.d.ts +60 -0
- package/packages/server/dist/services/harnessCommandService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessCommandService.js +853 -0
- package/packages/server/dist/services/harnessCommandService.js.map +1 -0
- package/packages/server/dist/services/harnessHookService.d.ts +55 -0
- package/packages/server/dist/services/harnessHookService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessHookService.js +1060 -0
- package/packages/server/dist/services/harnessHookService.js.map +1 -0
- package/packages/server/dist/services/harnessLintService.d.ts +49 -0
- package/packages/server/dist/services/harnessLintService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessLintService.js +628 -0
- package/packages/server/dist/services/harnessLintService.js.map +1 -0
- package/packages/server/dist/services/harnessMcpService.d.ts +77 -0
- package/packages/server/dist/services/harnessMcpService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessMcpService.js +814 -0
- package/packages/server/dist/services/harnessMcpService.js.map +1 -0
- package/packages/server/dist/services/harnessPluginService.d.ts +66 -0
- package/packages/server/dist/services/harnessPluginService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessPluginService.js +559 -0
- package/packages/server/dist/services/harnessPluginService.js.map +1 -0
- package/packages/server/dist/services/harnessService.d.ts +40 -0
- package/packages/server/dist/services/harnessService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessService.js +222 -0
- package/packages/server/dist/services/harnessService.js.map +1 -0
- package/packages/server/dist/services/harnessShareScopeService.d.ts +31 -0
- package/packages/server/dist/services/harnessShareScopeService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessShareScopeService.js +93 -0
- package/packages/server/dist/services/harnessShareScopeService.js.map +1 -0
- package/packages/server/dist/services/harnessSkillService.d.ts +70 -0
- package/packages/server/dist/services/harnessSkillService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessSkillService.js +636 -0
- package/packages/server/dist/services/harnessSkillService.js.map +1 -0
- package/packages/server/dist/services/historyParser.d.ts +4 -14
- package/packages/server/dist/services/historyParser.d.ts.map +1 -1
- package/packages/server/dist/services/historyParser.js +60 -5
- package/packages/server/dist/services/historyParser.js.map +1 -1
- package/packages/server/dist/services/issueService.d.ts.map +1 -1
- package/packages/server/dist/services/issueService.js +10 -2
- package/packages/server/dist/services/issueService.js.map +1 -1
- package/packages/server/dist/services/manualSyncService.d.ts +19 -0
- package/packages/server/dist/services/manualSyncService.d.ts.map +1 -0
- package/packages/server/dist/services/manualSyncService.js +110 -0
- package/packages/server/dist/services/manualSyncService.js.map +1 -0
- package/packages/server/dist/services/notificationService.d.ts.map +1 -1
- package/packages/server/dist/services/notificationService.js +34 -9
- package/packages/server/dist/services/notificationService.js.map +1 -1
- package/packages/server/dist/services/preferencesService.d.ts.map +1 -1
- package/packages/server/dist/services/preferencesService.js +8 -1
- package/packages/server/dist/services/preferencesService.js.map +1 -1
- package/packages/server/dist/services/projectService.d.ts +5 -0
- package/packages/server/dist/services/projectService.d.ts.map +1 -1
- package/packages/server/dist/services/projectService.js +42 -2
- package/packages/server/dist/services/projectService.js.map +1 -1
- package/packages/server/dist/services/ptyService.d.ts +1 -0
- package/packages/server/dist/services/ptyService.d.ts.map +1 -1
- package/packages/server/dist/services/ptyService.js +36 -5
- package/packages/server/dist/services/ptyService.js.map +1 -1
- package/packages/server/dist/services/queueService.d.ts.map +1 -1
- package/packages/server/dist/services/queueService.js +83 -14
- package/packages/server/dist/services/queueService.js.map +1 -1
- package/packages/server/dist/services/sessionBufferManager.d.ts.map +1 -1
- package/packages/server/dist/services/sessionBufferManager.js +26 -0
- package/packages/server/dist/services/sessionBufferManager.js.map +1 -1
- package/packages/server/dist/services/sessionService.d.ts +4 -3
- package/packages/server/dist/services/sessionService.d.ts.map +1 -1
- package/packages/server/dist/services/sessionService.js +5 -4
- package/packages/server/dist/services/sessionService.js.map +1 -1
- package/packages/server/dist/services/snippetService.d.ts +54 -0
- package/packages/server/dist/services/snippetService.d.ts.map +1 -0
- package/packages/server/dist/services/snippetService.js +371 -0
- package/packages/server/dist/services/snippetService.js.map +1 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts +46 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts.map +1 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js +125 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js.map +1 -0
- package/packages/server/dist/services/webPushService.d.ts.map +1 -1
- package/packages/server/dist/services/webPushService.js +8 -1
- package/packages/server/dist/services/webPushService.js.map +1 -1
- package/packages/server/dist/snippets/split-commit +9 -0
- package/packages/server/dist/utils/applySecretsPolicy.d.ts +53 -0
- package/packages/server/dist/utils/applySecretsPolicy.d.ts.map +1 -0
- package/packages/server/dist/utils/applySecretsPolicy.js +204 -0
- package/packages/server/dist/utils/applySecretsPolicy.js.map +1 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.d.ts +40 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.d.ts.map +1 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.js +47 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.js.map +1 -0
- package/packages/server/dist/utils/effortUtils.d.ts +21 -0
- package/packages/server/dist/utils/effortUtils.d.ts.map +1 -0
- package/packages/server/dist/utils/effortUtils.js +36 -0
- package/packages/server/dist/utils/effortUtils.js.map +1 -0
- package/packages/server/dist/utils/gitignoreFilter.d.ts +23 -0
- package/packages/server/dist/utils/gitignoreFilter.d.ts.map +1 -0
- package/packages/server/dist/utils/gitignoreFilter.js +42 -0
- package/packages/server/dist/utils/gitignoreFilter.js.map +1 -0
- package/packages/server/dist/utils/harnessBundleSchema.d.ts +105 -0
- package/packages/server/dist/utils/harnessBundleSchema.d.ts.map +1 -0
- package/packages/server/dist/utils/harnessBundleSchema.js +79 -0
- package/packages/server/dist/utils/harnessBundleSchema.js.map +1 -0
- package/packages/server/dist/utils/harnessPaths.d.ts +34 -0
- package/packages/server/dist/utils/harnessPaths.d.ts.map +1 -0
- package/packages/server/dist/utils/harnessPaths.js +124 -0
- package/packages/server/dist/utils/harnessPaths.js.map +1 -0
- package/packages/server/dist/utils/pathUtils.d.ts +3 -2
- package/packages/server/dist/utils/pathUtils.d.ts.map +1 -1
- package/packages/server/dist/utils/pathUtils.js +26 -2
- package/packages/server/dist/utils/pathUtils.js.map +1 -1
- package/packages/server/dist/utils/secretHeuristic.d.ts +72 -0
- package/packages/server/dist/utils/secretHeuristic.d.ts.map +1 -0
- package/packages/server/dist/utils/secretHeuristic.js +163 -0
- package/packages/server/dist/utils/secretHeuristic.js.map +1 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.d.ts +41 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.d.ts.map +1 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.js +81 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.js.map +1 -0
- package/packages/server/dist/utils/serverPathResolver.d.ts +29 -0
- package/packages/server/dist/utils/serverPathResolver.d.ts.map +1 -0
- package/packages/server/dist/utils/serverPathResolver.js +59 -0
- package/packages/server/dist/utils/serverPathResolver.js.map +1 -0
- package/packages/server/dist/utils/snippetPaths.d.ts +61 -0
- package/packages/server/dist/utils/snippetPaths.d.ts.map +1 -0
- package/packages/server/dist/utils/snippetPaths.js +123 -0
- package/packages/server/dist/utils/snippetPaths.js.map +1 -0
- package/packages/server/dist/utils/structuredEditor.d.ts +34 -0
- package/packages/server/dist/utils/structuredEditor.d.ts.map +1 -0
- package/packages/server/dist/utils/structuredEditor.js +111 -0
- package/packages/server/dist/utils/structuredEditor.js.map +1 -0
- package/packages/server/package.json +6 -2
- package/packages/server/resources/internals/INDEX.md +23 -0
- package/packages/server/resources/internals/harness-files.md +63 -0
- package/packages/server/resources/internals/image-storage.md +43 -0
- package/packages/server/resources/manual/01-getting-started.md +104 -0
- package/packages/server/resources/manual/02-chat.md +285 -0
- package/packages/server/resources/manual/03-sessions.md +48 -0
- package/packages/server/resources/manual/04-slash-commands-favorites.md +152 -0
- package/packages/server/resources/manual/05-projects.md +74 -0
- package/packages/server/resources/manual/06-file-explorer-editor.md +90 -0
- package/packages/server/resources/manual/07-git.md +94 -0
- package/packages/server/resources/manual/08-terminal.md +59 -0
- package/packages/server/resources/manual/09-queue-runner.md +262 -0
- package/packages/server/resources/manual/10-project-board.md +193 -0
- package/packages/server/resources/manual/11-bmad-method-integration.md +128 -0
- package/packages/server/resources/manual/12-harness-workbench.md +175 -0
- package/packages/server/resources/manual/13-settings.md +241 -0
- package/packages/server/resources/manual/14-keyboard-shortcuts.md +68 -0
- package/packages/server/resources/manual/15-environment-variables.md +28 -0
- package/packages/server/resources/manual/16-troubleshooting.md +110 -0
- package/packages/server/resources/manual/INDEX.md +60 -0
- package/packages/shared/dist/index.d.ts +3 -0
- package/packages/shared/dist/index.d.ts.map +1 -1
- package/packages/shared/dist/index.js +6 -0
- package/packages/shared/dist/index.js.map +1 -1
- package/packages/shared/dist/types/command.d.ts +3 -3
- package/packages/shared/dist/types/command.d.ts.map +1 -1
- package/packages/shared/dist/types/fileSystem.d.ts +19 -0
- package/packages/shared/dist/types/fileSystem.d.ts.map +1 -1
- package/packages/shared/dist/types/fileSystem.js +5 -0
- package/packages/shared/dist/types/fileSystem.js.map +1 -1
- package/packages/shared/dist/types/git.d.ts +6 -1
- package/packages/shared/dist/types/git.d.ts.map +1 -1
- package/packages/shared/dist/types/git.js.map +1 -1
- package/packages/shared/dist/types/harness.d.ts +1211 -0
- package/packages/shared/dist/types/harness.d.ts.map +1 -0
- package/packages/shared/dist/types/harness.js +107 -0
- package/packages/shared/dist/types/harness.js.map +1 -0
- package/packages/shared/dist/types/harnessBundle.d.ts +170 -0
- package/packages/shared/dist/types/harnessBundle.d.ts.map +1 -0
- package/packages/shared/dist/types/harnessBundle.js +18 -0
- package/packages/shared/dist/types/harnessBundle.js.map +1 -0
- package/packages/shared/dist/types/history.d.ts +7 -0
- package/packages/shared/dist/types/history.d.ts.map +1 -1
- package/packages/shared/dist/types/preferences.d.ts +4 -1
- package/packages/shared/dist/types/preferences.d.ts.map +1 -1
- package/packages/shared/dist/types/preferences.js +1 -0
- package/packages/shared/dist/types/preferences.js.map +1 -1
- package/packages/shared/dist/types/queue.d.ts +9 -0
- package/packages/shared/dist/types/queue.d.ts.map +1 -1
- package/packages/shared/dist/types/sdk.d.ts +42 -1
- package/packages/shared/dist/types/sdk.d.ts.map +1 -1
- package/packages/shared/dist/types/sdk.js +26 -2
- package/packages/shared/dist/types/sdk.js.map +1 -1
- package/packages/shared/dist/types/websocket.d.ts +24 -0
- package/packages/shared/dist/types/websocket.d.ts.map +1 -1
- package/packages/shared/dist/utils/markdownSections.d.ts +50 -0
- package/packages/shared/dist/utils/markdownSections.d.ts.map +1 -0
- package/packages/shared/dist/utils/markdownSections.js +111 -0
- package/packages/shared/dist/utils/markdownSections.js.map +1 -0
- package/packages/shared/dist/utils/queueParser.d.ts.map +1 -1
- package/packages/shared/dist/utils/queueParser.js +104 -0
- package/packages/shared/dist/utils/queueParser.js.map +1 -1
- package/scripts/build-manual-shards.mjs +100 -0
- package/scripts/mock-telegram.mjs +172 -0
- package/scripts/run-integration-test.mjs +362 -0
- package/packages/client/dist/assets/index-Bf0D9oVJ.css +0 -32
- package/packages/client/dist/assets/index-CRmzoqHy.js +0 -2
- package/packages/client/dist/assets/index-CszGQ29O.js +0 -1432
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 28.6: Harness sub-agent service.
|
|
3
|
+
*
|
|
4
|
+
* Combines three sources of `.claude/agents/*.md` files into a single flat
|
|
5
|
+
* card list (flat-only — no recursive subdirectory walk):
|
|
6
|
+
* - <projectRoot>/.claude/agents/ (project scope)
|
|
7
|
+
* - ~/.claude/agents/ (user scope)
|
|
8
|
+
* - <pluginInstallPath>/agents/ (plugin scope, read-only)
|
|
9
|
+
*
|
|
10
|
+
* Each .md file has YAML frontmatter with 4 required fields
|
|
11
|
+
* (name/description/model/color) + 1 optional 3-state `tools` field. Body is
|
|
12
|
+
* the markdown system prompt. Round-trip via `applyYamlFrontmatterPatch`
|
|
13
|
+
* (eemeli yaml — preserves comments / key order / blank lines).
|
|
14
|
+
*
|
|
15
|
+
* Differences from 28.5 (commands):
|
|
16
|
+
* - flat-only scan (no subdirectory walk)
|
|
17
|
+
* - 4 required frontmatter fields (name regex + model/color enum)
|
|
18
|
+
* - tools 3-state model (omitted vs empty vs populated) round-trip
|
|
19
|
+
* - file stem === frontmatter.name (no slash-name conversion)
|
|
20
|
+
* - no chat slash palette dedup
|
|
21
|
+
* - no BMad mirror heuristic (`.claude/agents/` has no BMad presence)
|
|
22
|
+
*/
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import fs from 'fs/promises';
|
|
25
|
+
import yaml from 'yaml';
|
|
26
|
+
import { parseDocument } from 'yaml';
|
|
27
|
+
import { HARNESS_ERRORS, } from '@hammoc/shared';
|
|
28
|
+
import { harnessService } from './harnessService.js';
|
|
29
|
+
import { projectService } from './projectService.js';
|
|
30
|
+
import { getUserHarnessRoot } from '../utils/harnessPaths.js';
|
|
31
|
+
import { detectSecretsInText as detectSecretsInTextCanonical } from '../utils/secretHeuristic.js';
|
|
32
|
+
import { assertNoSecretOnShared } from '../utils/assertNoSecretOnShared.js';
|
|
33
|
+
import { splitFrontmatterAndBody } from './utils/applyYamlFrontmatterPatch.js';
|
|
34
|
+
const SCOPE_PRIORITY = {
|
|
35
|
+
project: 0,
|
|
36
|
+
user: 1,
|
|
37
|
+
plugin: 2,
|
|
38
|
+
};
|
|
39
|
+
const ALLOWED_MODELS = new Set([
|
|
40
|
+
'inherit',
|
|
41
|
+
'sonnet',
|
|
42
|
+
'opus',
|
|
43
|
+
'haiku',
|
|
44
|
+
]);
|
|
45
|
+
const ALLOWED_COLORS = new Set([
|
|
46
|
+
'blue',
|
|
47
|
+
'cyan',
|
|
48
|
+
'green',
|
|
49
|
+
'yellow',
|
|
50
|
+
'magenta',
|
|
51
|
+
'red',
|
|
52
|
+
]);
|
|
53
|
+
const AGENTS_DIR = 'agents';
|
|
54
|
+
// Story 30.1 (Task 1.2): SECRET_PATTERNS / ENV_REF_RE moved to
|
|
55
|
+
// `utils/secretHeuristic.ts`. The wrapper below adapts the canonical entry
|
|
56
|
+
// point to this service's existing `{ matched, lines }` shape.
|
|
57
|
+
const PLUGIN_ROOT_RE = /\$\{CLAUDE_PLUGIN_ROOT\}/;
|
|
58
|
+
const EXAMPLE_BLOCK_RE = /<example[\s>][\s\S]*?<\/example>/i;
|
|
59
|
+
/**
|
|
60
|
+
* Agent name regex: 3-50 chars, lowercase letters / digits / hyphens, must
|
|
61
|
+
* start with a lowercase letter and end with letter-or-digit (cannot start or
|
|
62
|
+
* end with a hyphen).
|
|
63
|
+
*/
|
|
64
|
+
const AGENT_NAME_RE = /^[a-z][a-z0-9-]{1,48}[a-z0-9]$/;
|
|
65
|
+
// eslint-disable-next-line no-control-regex
|
|
66
|
+
const RESERVED_CHARS_RE = /[\\/<>:"|?*\x00-\x1F]/;
|
|
67
|
+
const TRAILING_DOT_OR_SPACE_RE = /[. ]$/;
|
|
68
|
+
function throwMapped(code, message, extras) {
|
|
69
|
+
const err = new Error(message);
|
|
70
|
+
err.code = code;
|
|
71
|
+
if (extras)
|
|
72
|
+
Object.assign(err, extras);
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
function isFileNotFound(err) {
|
|
76
|
+
const code = err?.code;
|
|
77
|
+
return code === 'ENOENT' || code === HARNESS_ERRORS.HARNESS_FILE_NOT_FOUND.code;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Validation helpers
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/** Throws HARNESS_PARSE_ERROR with detail when name fails the regex / OS check. */
|
|
83
|
+
function validateAgentName(name) {
|
|
84
|
+
if (!name || typeof name !== 'string') {
|
|
85
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'name is required', {
|
|
86
|
+
detail: 'invalid-name-pattern',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (!AGENT_NAME_RE.test(name)) {
|
|
90
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'name pattern invalid', {
|
|
91
|
+
detail: 'invalid-name-pattern',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (RESERVED_CHARS_RE.test(name)) {
|
|
95
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'OS-reserved characters not allowed', {
|
|
96
|
+
detail: 'invalid-name-pattern',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (TRAILING_DOT_OR_SPACE_RE.test(name)) {
|
|
100
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'name cannot end with space or dot', {
|
|
101
|
+
detail: 'invalid-name-pattern',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (name.includes('..')) {
|
|
105
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'path traversal denied');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Token / secret helpers
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
function detectExampleBlock(body) {
|
|
112
|
+
return EXAMPLE_BLOCK_RE.test(body);
|
|
113
|
+
}
|
|
114
|
+
function detectSecretsInText(text) {
|
|
115
|
+
const { matched, lines } = detectSecretsInTextCanonical(text);
|
|
116
|
+
return { matched, lines };
|
|
117
|
+
}
|
|
118
|
+
function parseAgentFrontmatter(raw, fileStem) {
|
|
119
|
+
if (raw === null) {
|
|
120
|
+
return { malformed: true, reason: 'invalid-frontmatter', detail: 'missing frontmatter' };
|
|
121
|
+
}
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = yaml.parse(raw);
|
|
125
|
+
}
|
|
126
|
+
catch (cause) {
|
|
127
|
+
return {
|
|
128
|
+
malformed: true,
|
|
129
|
+
reason: 'invalid-frontmatter',
|
|
130
|
+
detail: cause.message,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (parsed === null || parsed === undefined) {
|
|
134
|
+
return { malformed: true, reason: 'invalid-frontmatter', detail: 'empty frontmatter' };
|
|
135
|
+
}
|
|
136
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
137
|
+
return {
|
|
138
|
+
malformed: true,
|
|
139
|
+
reason: 'invalid-frontmatter',
|
|
140
|
+
detail: 'frontmatter must be a mapping',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const obj = parsed;
|
|
144
|
+
// name
|
|
145
|
+
if (typeof obj.name !== 'string' || obj.name.length === 0) {
|
|
146
|
+
return { malformed: true, reason: 'invalid-frontmatter', detail: 'name required' };
|
|
147
|
+
}
|
|
148
|
+
if (!AGENT_NAME_RE.test(obj.name)) {
|
|
149
|
+
return { malformed: true, reason: 'invalid-name-pattern', detail: obj.name };
|
|
150
|
+
}
|
|
151
|
+
if (obj.name !== fileStem) {
|
|
152
|
+
return {
|
|
153
|
+
malformed: true,
|
|
154
|
+
reason: 'name-mismatch',
|
|
155
|
+
detail: `frontmatter.name=${obj.name} fileStem=${fileStem}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// description
|
|
159
|
+
if (typeof obj.description !== 'string' || obj.description.length === 0) {
|
|
160
|
+
return { malformed: true, reason: 'invalid-frontmatter', detail: 'description required' };
|
|
161
|
+
}
|
|
162
|
+
// model
|
|
163
|
+
if (typeof obj.model !== 'string' || !ALLOWED_MODELS.has(obj.model)) {
|
|
164
|
+
return { malformed: true, reason: 'invalid-model', detail: String(obj.model) };
|
|
165
|
+
}
|
|
166
|
+
// color
|
|
167
|
+
if (typeof obj.color !== 'string' || !ALLOWED_COLORS.has(obj.color)) {
|
|
168
|
+
return { malformed: true, reason: 'invalid-color', detail: String(obj.color) };
|
|
169
|
+
}
|
|
170
|
+
// tools (3-state)
|
|
171
|
+
let tools;
|
|
172
|
+
let toolsState;
|
|
173
|
+
if (!('tools' in obj)) {
|
|
174
|
+
tools = undefined;
|
|
175
|
+
toolsState = 'omitted';
|
|
176
|
+
}
|
|
177
|
+
else if (Array.isArray(obj.tools)) {
|
|
178
|
+
if (obj.tools.length === 0) {
|
|
179
|
+
tools = [];
|
|
180
|
+
toolsState = 'empty';
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const allStrings = obj.tools.every((t) => typeof t === 'string' && t.length > 0);
|
|
184
|
+
if (!allStrings) {
|
|
185
|
+
return {
|
|
186
|
+
malformed: true,
|
|
187
|
+
reason: 'invalid-frontmatter',
|
|
188
|
+
detail: 'tools entries must be strings',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
tools = obj.tools;
|
|
192
|
+
toolsState = 'populated';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
return {
|
|
197
|
+
malformed: true,
|
|
198
|
+
reason: 'invalid-frontmatter',
|
|
199
|
+
detail: 'tools must be an array if present',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const frontmatter = {
|
|
203
|
+
name: obj.name,
|
|
204
|
+
description: obj.description,
|
|
205
|
+
model: obj.model,
|
|
206
|
+
color: obj.color,
|
|
207
|
+
...(tools !== undefined ? { tools } : {}),
|
|
208
|
+
};
|
|
209
|
+
return { frontmatter, toolsState };
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Roots / containment
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
function userAgentsRoot() {
|
|
215
|
+
return path.join(getUserHarnessRoot(), AGENTS_DIR);
|
|
216
|
+
}
|
|
217
|
+
async function projectAgentsRoot(projectSlug) {
|
|
218
|
+
const projectRoot = await projectService.resolveOriginalPath(projectSlug);
|
|
219
|
+
return path.join(projectRoot, '.claude', AGENTS_DIR);
|
|
220
|
+
}
|
|
221
|
+
function pluginAgentsRoots(installPath) {
|
|
222
|
+
return [path.join(installPath, AGENTS_DIR)];
|
|
223
|
+
}
|
|
224
|
+
function withinRoot(absolute, root) {
|
|
225
|
+
const resolved = path.resolve(absolute);
|
|
226
|
+
const resolvedRoot = path.resolve(root);
|
|
227
|
+
return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep);
|
|
228
|
+
}
|
|
229
|
+
async function readInstalledPlugins() {
|
|
230
|
+
try {
|
|
231
|
+
const res = await harnessService.read({
|
|
232
|
+
scope: 'user',
|
|
233
|
+
relativePath: 'plugins/installed_plugins.json',
|
|
234
|
+
});
|
|
235
|
+
const trimmed = (res.content ?? '').trim();
|
|
236
|
+
if (!trimmed)
|
|
237
|
+
return {};
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(trimmed);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
if (isFileNotFound(err))
|
|
247
|
+
return {};
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Read a plugin's manifest `agents` field (when present). Returns relative posix paths. */
|
|
252
|
+
async function readPluginManifestAgents(installPath) {
|
|
253
|
+
const manifestPath = path.join(installPath, '.claude-plugin', 'plugin.json');
|
|
254
|
+
let raw;
|
|
255
|
+
try {
|
|
256
|
+
raw = await fs.readFile(manifestPath, 'utf-8');
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
let parsed;
|
|
262
|
+
try {
|
|
263
|
+
parsed = JSON.parse(raw);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
269
|
+
return null;
|
|
270
|
+
const agents = parsed.agents;
|
|
271
|
+
if (!Array.isArray(agents))
|
|
272
|
+
return null;
|
|
273
|
+
const out = [];
|
|
274
|
+
for (const entry of agents) {
|
|
275
|
+
if (typeof entry === 'string' && entry.length > 0)
|
|
276
|
+
out.push(entry);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
/** Flat-only scan — list .md files directly under `root` (no recursion). */
|
|
281
|
+
async function listFlatMdFiles(root) {
|
|
282
|
+
let entries;
|
|
283
|
+
try {
|
|
284
|
+
entries = await fs.readdir(root, { withFileTypes: true });
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
const out = [];
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
292
|
+
out.push(path.join(root, entry.name));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
async function readAndParseAgent(absolute) {
|
|
298
|
+
let stat;
|
|
299
|
+
try {
|
|
300
|
+
stat = await fs.stat(absolute);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
if (err.code === 'ENOENT') {
|
|
304
|
+
return { malformed: true, reason: 'invalid-frontmatter', detail: 'file not found' };
|
|
305
|
+
}
|
|
306
|
+
throw err;
|
|
307
|
+
}
|
|
308
|
+
if (!stat.isFile()) {
|
|
309
|
+
return { malformed: true, reason: 'invalid-frontmatter', detail: 'not a regular file' };
|
|
310
|
+
}
|
|
311
|
+
let raw;
|
|
312
|
+
try {
|
|
313
|
+
raw = await fs.readFile(absolute, 'utf-8');
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return { malformed: true, reason: 'invalid-frontmatter', detail: 'failed to read' };
|
|
317
|
+
}
|
|
318
|
+
const fileStem = path.basename(absolute, '.md');
|
|
319
|
+
const { frontmatterRaw, body } = splitFrontmatterAndBody(raw);
|
|
320
|
+
const parsed = parseAgentFrontmatter(frontmatterRaw, fileStem);
|
|
321
|
+
if ('malformed' in parsed) {
|
|
322
|
+
return parsed;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
raw,
|
|
326
|
+
mtime: stat.mtime.toISOString(),
|
|
327
|
+
frontmatter: parsed.frontmatter,
|
|
328
|
+
body,
|
|
329
|
+
toolsState: parsed.toolsState,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function makeCard(scope, fileAbs, frontmatter, body, mtime, toolsState, extra) {
|
|
333
|
+
const fileStem = path.basename(fileAbs, '.md');
|
|
334
|
+
return {
|
|
335
|
+
scope,
|
|
336
|
+
absoluteFile: fileAbs,
|
|
337
|
+
pluginKey: extra.pluginKey,
|
|
338
|
+
projectSlug: extra.projectSlug,
|
|
339
|
+
name: fileStem,
|
|
340
|
+
description: frontmatter.description,
|
|
341
|
+
model: frontmatter.model,
|
|
342
|
+
color: frontmatter.color,
|
|
343
|
+
toolsState,
|
|
344
|
+
tools: toolsState === 'populated' ? (frontmatter.tools ?? []) : [],
|
|
345
|
+
hasExampleBlock: detectExampleBlock(body),
|
|
346
|
+
mtime,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function enumerateAgentsInDirectory(scope, rootAbs, extra) {
|
|
350
|
+
const files = await listFlatMdFiles(rootAbs);
|
|
351
|
+
const reads = files.map(async (abs) => {
|
|
352
|
+
const result = await readAndParseAgent(abs);
|
|
353
|
+
if ('malformed' in result) {
|
|
354
|
+
return {
|
|
355
|
+
kind: 'malformed',
|
|
356
|
+
entry: { abs, reason: result.reason, detail: result.detail },
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
kind: 'card',
|
|
361
|
+
card: makeCard(scope, abs, result.frontmatter, '', result.mtime, result.toolsState, extra),
|
|
362
|
+
body: result.body,
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
const settled = await Promise.all(reads);
|
|
366
|
+
const cards = [];
|
|
367
|
+
const malformed = [];
|
|
368
|
+
for (const r of settled) {
|
|
369
|
+
if (r.kind === 'card') {
|
|
370
|
+
// Re-derive hasExampleBlock from body (we passed empty body to makeCard above
|
|
371
|
+
// to avoid double-walking the body). Simpler: just reconstruct here.
|
|
372
|
+
const reconstructed = {
|
|
373
|
+
...r.card,
|
|
374
|
+
hasExampleBlock: detectExampleBlock(r.body),
|
|
375
|
+
};
|
|
376
|
+
cards.push(reconstructed);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
malformed.push({
|
|
380
|
+
scope,
|
|
381
|
+
absoluteFile: r.entry.abs,
|
|
382
|
+
pluginKey: extra.pluginKey,
|
|
383
|
+
projectSlug: extra.projectSlug,
|
|
384
|
+
reason: r.entry.reason,
|
|
385
|
+
detail: r.entry.detail,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { cards, malformed };
|
|
390
|
+
}
|
|
391
|
+
async function enumeratePluginAgents() {
|
|
392
|
+
const installed = await readInstalledPlugins();
|
|
393
|
+
const plugins = installed.plugins ?? {};
|
|
394
|
+
const tasks = [];
|
|
395
|
+
for (const [pluginKey, value] of Object.entries(plugins)) {
|
|
396
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
if (!entry?.installPath || typeof entry.installPath !== 'string')
|
|
399
|
+
continue;
|
|
400
|
+
const installRoot = path.resolve(entry.installPath);
|
|
401
|
+
tasks.push((async () => {
|
|
402
|
+
// Manifest-driven enumeration takes priority — when plugin.json
|
|
403
|
+
// declares `agents` paths, treat those as the source of truth.
|
|
404
|
+
const manifestPaths = await readPluginManifestAgents(entry.installPath);
|
|
405
|
+
if (manifestPaths && manifestPaths.length > 0) {
|
|
406
|
+
const cards = [];
|
|
407
|
+
const malformed = [];
|
|
408
|
+
for (const relPosix of manifestPaths) {
|
|
409
|
+
const abs = path.resolve(entry.installPath, relPosix.split('/').join(path.sep));
|
|
410
|
+
if (!withinRoot(abs, installRoot))
|
|
411
|
+
continue;
|
|
412
|
+
const parsed = await readAndParseAgent(abs);
|
|
413
|
+
if ('malformed' in parsed) {
|
|
414
|
+
malformed.push({
|
|
415
|
+
scope: 'plugin',
|
|
416
|
+
absoluteFile: abs,
|
|
417
|
+
pluginKey,
|
|
418
|
+
reason: parsed.reason,
|
|
419
|
+
detail: parsed.detail,
|
|
420
|
+
});
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
cards.push(makeCard('plugin', abs, parsed.frontmatter, parsed.body, parsed.mtime, parsed.toolsState, { pluginKey }));
|
|
424
|
+
}
|
|
425
|
+
return { cards, malformed };
|
|
426
|
+
}
|
|
427
|
+
// Fall back to flat scan of <installPath>/agents/.
|
|
428
|
+
const collected = { cards: [], malformed: [] };
|
|
429
|
+
for (const root of pluginAgentsRoots(entry.installPath)) {
|
|
430
|
+
try {
|
|
431
|
+
const stat = await fs.stat(root);
|
|
432
|
+
if (!stat.isDirectory())
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (!withinRoot(root, installRoot))
|
|
439
|
+
continue;
|
|
440
|
+
const part = await enumerateAgentsInDirectory('plugin', root, { pluginKey });
|
|
441
|
+
// Defensive containment filter.
|
|
442
|
+
for (const card of part.cards) {
|
|
443
|
+
if (withinRoot(card.absoluteFile, installRoot))
|
|
444
|
+
collected.cards.push(card);
|
|
445
|
+
}
|
|
446
|
+
for (const m of part.malformed) {
|
|
447
|
+
if (withinRoot(m.absoluteFile, installRoot))
|
|
448
|
+
collected.malformed.push(m);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return collected;
|
|
452
|
+
})());
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const settled = await Promise.all(tasks);
|
|
456
|
+
const cards = [];
|
|
457
|
+
const malformed = [];
|
|
458
|
+
for (const part of settled) {
|
|
459
|
+
cards.push(...part.cards);
|
|
460
|
+
malformed.push(...part.malformed);
|
|
461
|
+
}
|
|
462
|
+
return { cards, malformed };
|
|
463
|
+
}
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
// Source resolution
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
async function resolveAgentRoot(scope, projectSlug, pluginKey) {
|
|
468
|
+
if (scope === 'project') {
|
|
469
|
+
if (!projectSlug) {
|
|
470
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for scope=project');
|
|
471
|
+
}
|
|
472
|
+
return { root: await projectAgentsRoot(projectSlug) };
|
|
473
|
+
}
|
|
474
|
+
if (scope === 'user') {
|
|
475
|
+
return { root: userAgentsRoot() };
|
|
476
|
+
}
|
|
477
|
+
if (!pluginKey) {
|
|
478
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'pluginKey required for scope=plugin');
|
|
479
|
+
}
|
|
480
|
+
const installed = await readInstalledPlugins();
|
|
481
|
+
const raw = installed.plugins?.[pluginKey];
|
|
482
|
+
const entries = raw ? (Array.isArray(raw) ? raw : [raw]) : [];
|
|
483
|
+
const first = entries.find((e) => typeof e?.installPath === 'string');
|
|
484
|
+
if (!first?.installPath) {
|
|
485
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PLUGIN_NOT_FOUND.code, `plugin not installed: ${pluginKey}`);
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
root: path.join(first.installPath, AGENTS_DIR),
|
|
489
|
+
pluginInstallRoot: path.resolve(first.installPath),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
async function resolveAbsoluteFile(scope, name, projectSlug, pluginKey) {
|
|
493
|
+
validateAgentName(name);
|
|
494
|
+
// For plugins where the manifest may specify a non-flat path, we need to
|
|
495
|
+
// prefer the manifest mapping over the default flat layout.
|
|
496
|
+
if (scope === 'plugin' && pluginKey) {
|
|
497
|
+
const installed = await readInstalledPlugins();
|
|
498
|
+
const raw = installed.plugins?.[pluginKey];
|
|
499
|
+
const entries = raw ? (Array.isArray(raw) ? raw : [raw]) : [];
|
|
500
|
+
const first = entries.find((e) => typeof e?.installPath === 'string');
|
|
501
|
+
if (!first?.installPath) {
|
|
502
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PLUGIN_NOT_FOUND.code, `plugin not installed: ${pluginKey}`);
|
|
503
|
+
}
|
|
504
|
+
const installRoot = path.resolve(first.installPath);
|
|
505
|
+
const manifest = await readPluginManifestAgents(first.installPath);
|
|
506
|
+
if (manifest) {
|
|
507
|
+
const match = manifest.find((rel) => path.basename(rel, '.md') === name);
|
|
508
|
+
if (match) {
|
|
509
|
+
const abs = path.resolve(first.installPath, match.split('/').join(path.sep));
|
|
510
|
+
if (!withinRoot(abs, installRoot)) {
|
|
511
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'plugin file escapes installPath');
|
|
512
|
+
}
|
|
513
|
+
return { abs, root: path.dirname(abs), pluginInstallRoot: installRoot };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const fallbackRoot = path.join(first.installPath, AGENTS_DIR);
|
|
517
|
+
const abs = path.resolve(fallbackRoot, `${name}.md`);
|
|
518
|
+
if (!withinRoot(abs, installRoot)) {
|
|
519
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'plugin file escapes installPath');
|
|
520
|
+
}
|
|
521
|
+
return { abs, root: fallbackRoot, pluginInstallRoot: installRoot };
|
|
522
|
+
}
|
|
523
|
+
const { root } = await resolveAgentRoot(scope, projectSlug);
|
|
524
|
+
const abs = path.resolve(root, `${name}.md`);
|
|
525
|
+
if (!withinRoot(abs, root)) {
|
|
526
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'path escapes agents root');
|
|
527
|
+
}
|
|
528
|
+
return { abs, root };
|
|
529
|
+
}
|
|
530
|
+
function makeSourceLocation(scope, abs, name, projectSlug, pluginKey) {
|
|
531
|
+
return {
|
|
532
|
+
scope,
|
|
533
|
+
absoluteFile: abs,
|
|
534
|
+
pluginKey,
|
|
535
|
+
projectSlug,
|
|
536
|
+
name,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
// Frontmatter serialization with 3-state tools handling
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
const FRONTMATTER_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n)?/;
|
|
543
|
+
/**
|
|
544
|
+
* Custom YAML round-trip that preserves comments / key order / blank lines for
|
|
545
|
+
* untouched keys, and explicitly handles the tools 3-state model:
|
|
546
|
+
* - state A (omitted) → delete tools key
|
|
547
|
+
* - state B (empty) → write tools: []
|
|
548
|
+
* - state C (populated) → write tools: [...]
|
|
549
|
+
*/
|
|
550
|
+
function serializeAgentFrontmatter(prevRaw, opts) {
|
|
551
|
+
const { frontmatter, toolsState } = opts;
|
|
552
|
+
const match = FRONTMATTER_RE.exec(prevRaw);
|
|
553
|
+
const eol = prevRaw.includes('\r\n') ? '\r\n' : '\n';
|
|
554
|
+
// Resolve effective tools state.
|
|
555
|
+
let effectiveToolsState;
|
|
556
|
+
if (Array.isArray(frontmatter.tools) && frontmatter.tools.length > 0) {
|
|
557
|
+
effectiveToolsState = 'populated';
|
|
558
|
+
}
|
|
559
|
+
else if (toolsState !== undefined) {
|
|
560
|
+
effectiveToolsState = toolsState;
|
|
561
|
+
}
|
|
562
|
+
else if (Array.isArray(frontmatter.tools) && frontmatter.tools.length === 0) {
|
|
563
|
+
effectiveToolsState = 'empty';
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
effectiveToolsState = 'omitted';
|
|
567
|
+
}
|
|
568
|
+
const writeKeys = (doc) => {
|
|
569
|
+
doc.setIn(['name'], frontmatter.name);
|
|
570
|
+
doc.setIn(['description'], frontmatter.description);
|
|
571
|
+
doc.setIn(['model'], frontmatter.model);
|
|
572
|
+
doc.setIn(['color'], frontmatter.color);
|
|
573
|
+
if (effectiveToolsState === 'omitted') {
|
|
574
|
+
if (doc.hasIn(['tools']))
|
|
575
|
+
doc.deleteIn(['tools']);
|
|
576
|
+
}
|
|
577
|
+
else if (effectiveToolsState === 'empty') {
|
|
578
|
+
doc.setIn(['tools'], []);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
doc.setIn(['tools'], frontmatter.tools ?? []);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
if (!match) {
|
|
585
|
+
// No prior frontmatter — emit a fresh block.
|
|
586
|
+
let doc;
|
|
587
|
+
try {
|
|
588
|
+
doc = parseDocument('', { keepSourceTokens: true });
|
|
589
|
+
}
|
|
590
|
+
catch (cause) {
|
|
591
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `failed to parse frontmatter: ${cause.message}`);
|
|
592
|
+
}
|
|
593
|
+
if (doc.contents == null) {
|
|
594
|
+
doc.contents = doc.createNode({});
|
|
595
|
+
}
|
|
596
|
+
writeKeys(doc);
|
|
597
|
+
const yamlText = doc.toString().replace(/\r?\n$/, '');
|
|
598
|
+
return `---${eol}${yamlText}${eol}---${eol}${prevRaw}`;
|
|
599
|
+
}
|
|
600
|
+
let doc;
|
|
601
|
+
try {
|
|
602
|
+
doc = parseDocument(match[1], { keepSourceTokens: true });
|
|
603
|
+
if (doc.errors.length > 0)
|
|
604
|
+
throw doc.errors[0];
|
|
605
|
+
}
|
|
606
|
+
catch (cause) {
|
|
607
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `failed to parse frontmatter: ${cause.message}`);
|
|
608
|
+
}
|
|
609
|
+
if (doc.contents == null) {
|
|
610
|
+
doc.contents = doc.createNode({});
|
|
611
|
+
}
|
|
612
|
+
writeKeys(doc);
|
|
613
|
+
const yamlText = doc.toString().replace(/\r?\n$/, '');
|
|
614
|
+
const sliceEol = match[0].includes('\r\n') ? '\r\n' : '\n';
|
|
615
|
+
return `---${sliceEol}${yamlText}${sliceEol}---${sliceEol}${prevRaw.slice(match[0].length)}`;
|
|
616
|
+
}
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
// Service
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
class HarnessAgentService {
|
|
621
|
+
async listCards(currentProjectSlug) {
|
|
622
|
+
const projectTask = currentProjectSlug
|
|
623
|
+
? (async () => {
|
|
624
|
+
try {
|
|
625
|
+
const root = await projectAgentsRoot(currentProjectSlug);
|
|
626
|
+
return enumerateAgentsInDirectory('project', root, {
|
|
627
|
+
projectSlug: currentProjectSlug,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
if (err?.code === HARNESS_ERRORS.HARNESS_ROOT_MISSING.code) {
|
|
632
|
+
return { cards: [], malformed: [] };
|
|
633
|
+
}
|
|
634
|
+
throw err;
|
|
635
|
+
}
|
|
636
|
+
})()
|
|
637
|
+
: Promise.resolve({ cards: [], malformed: [] });
|
|
638
|
+
const userTask = enumerateAgentsInDirectory('user', userAgentsRoot(), {});
|
|
639
|
+
const pluginTask = enumeratePluginAgents();
|
|
640
|
+
const [projectPart, userPart, pluginPart] = await Promise.all([
|
|
641
|
+
projectTask,
|
|
642
|
+
userTask,
|
|
643
|
+
pluginTask,
|
|
644
|
+
]);
|
|
645
|
+
const cards = [...projectPart.cards, ...userPart.cards, ...pluginPart.cards];
|
|
646
|
+
const malformed = [
|
|
647
|
+
...projectPart.malformed,
|
|
648
|
+
...userPart.malformed,
|
|
649
|
+
...pluginPart.malformed,
|
|
650
|
+
];
|
|
651
|
+
cards.sort((a, b) => {
|
|
652
|
+
const sd = SCOPE_PRIORITY[a.scope] - SCOPE_PRIORITY[b.scope];
|
|
653
|
+
if (sd !== 0)
|
|
654
|
+
return sd;
|
|
655
|
+
return a.name.localeCompare(b.name);
|
|
656
|
+
});
|
|
657
|
+
return { cards, malformed };
|
|
658
|
+
}
|
|
659
|
+
async readAgent(loc) {
|
|
660
|
+
const result = await readAndParseAgent(loc.absoluteFile);
|
|
661
|
+
if ('malformed' in result) {
|
|
662
|
+
throwMapped(HARNESS_ERRORS.HARNESS_AGENT_NOT_FOUND.code, 'agent not found or malformed');
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
source: loc,
|
|
666
|
+
frontmatter: result.frontmatter,
|
|
667
|
+
body: result.body,
|
|
668
|
+
raw: result.raw,
|
|
669
|
+
mtime: result.mtime,
|
|
670
|
+
toolsState: result.toolsState,
|
|
671
|
+
hasExampleBlock: detectExampleBlock(result.body),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
async createAgent(req) {
|
|
675
|
+
validateAgentName(req.name);
|
|
676
|
+
if (req.frontmatter.name !== req.name) {
|
|
677
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'frontmatter.name must equal name', { detail: 'name-mismatch' });
|
|
678
|
+
}
|
|
679
|
+
if (!req.frontmatter.description || req.frontmatter.description.length === 0) {
|
|
680
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'description is required', {
|
|
681
|
+
detail: 'description required',
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
if (!ALLOWED_MODELS.has(req.frontmatter.model)) {
|
|
685
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'invalid model', {
|
|
686
|
+
detail: 'invalid-model',
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (!ALLOWED_COLORS.has(req.frontmatter.color)) {
|
|
690
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'invalid color', {
|
|
691
|
+
detail: 'invalid-color',
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
const { abs } = await resolveAbsoluteFile(req.scope, req.name, req.projectSlug);
|
|
695
|
+
try {
|
|
696
|
+
await fs.stat(abs);
|
|
697
|
+
throwMapped(HARNESS_ERRORS.HARNESS_AGENT_NAME_CONFLICT.code, `agent already exists: ${req.name}`);
|
|
698
|
+
}
|
|
699
|
+
catch (err) {
|
|
700
|
+
if (err.code === HARNESS_ERRORS.HARNESS_AGENT_NAME_CONFLICT.code) {
|
|
701
|
+
throw err;
|
|
702
|
+
}
|
|
703
|
+
// ENOENT — proceed.
|
|
704
|
+
}
|
|
705
|
+
const body = req.body ?? '';
|
|
706
|
+
const initial = serializeAgentFrontmatter(body, {
|
|
707
|
+
frontmatter: req.frontmatter,
|
|
708
|
+
toolsState: req.toolsState,
|
|
709
|
+
});
|
|
710
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
711
|
+
const ref = await this.buildEditableRef(req.scope, req.name, req.projectSlug);
|
|
712
|
+
const written = await harnessService.write(ref, { content: initial });
|
|
713
|
+
return {
|
|
714
|
+
success: true,
|
|
715
|
+
source: makeSourceLocation(req.scope, abs, req.name, req.projectSlug),
|
|
716
|
+
mtime: written.mtime,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
async updateAgent(loc, body) {
|
|
720
|
+
if (loc.scope === 'plugin') {
|
|
721
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'plugin-scope agents are read-only');
|
|
722
|
+
}
|
|
723
|
+
const editableScope = loc.scope;
|
|
724
|
+
const provided = [body.frontmatter, body.body, body.raw].filter((x) => x !== undefined);
|
|
725
|
+
if (provided.length !== 1) {
|
|
726
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'exactly one of frontmatter / body / raw is required');
|
|
727
|
+
}
|
|
728
|
+
const ref = await this.buildEditableRef(editableScope, loc.name, loc.projectSlug);
|
|
729
|
+
const current = await harnessService.read(ref);
|
|
730
|
+
const sourceText = current.content ?? '';
|
|
731
|
+
if (body.expectedMtime !== undefined && body.expectedMtime !== current.mtime) {
|
|
732
|
+
throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
|
|
733
|
+
currentMtime: current.mtime,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
let nextText;
|
|
737
|
+
if (body.frontmatter !== undefined) {
|
|
738
|
+
// Validate frontmatter required fields.
|
|
739
|
+
const fm = body.frontmatter;
|
|
740
|
+
if (fm.name !== loc.name) {
|
|
741
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'frontmatter.name cannot be changed via update — use copy + delete', { cause: 'name-rename-not-allowed' });
|
|
742
|
+
}
|
|
743
|
+
if (!fm.description || fm.description.length === 0) {
|
|
744
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'description required', {
|
|
745
|
+
detail: 'description required',
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
if (!ALLOWED_MODELS.has(fm.model)) {
|
|
749
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'invalid model', {
|
|
750
|
+
detail: 'invalid-model',
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
if (!ALLOWED_COLORS.has(fm.color)) {
|
|
754
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'invalid color', {
|
|
755
|
+
detail: 'invalid-color',
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
nextText = serializeAgentFrontmatter(sourceText, {
|
|
759
|
+
frontmatter: fm,
|
|
760
|
+
toolsState: body.toolsState,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
else if (body.body !== undefined) {
|
|
764
|
+
const re = /^---\s*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n)?/;
|
|
765
|
+
const match = re.exec(sourceText);
|
|
766
|
+
const head = match ? sourceText.slice(0, match[0].length) : '';
|
|
767
|
+
nextText = `${head}${body.body}`;
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
// raw — re-validate the frontmatter's 4 required fields.
|
|
771
|
+
const { frontmatterRaw } = splitFrontmatterAndBody(body.raw);
|
|
772
|
+
const parsed = parseAgentFrontmatter(frontmatterRaw, loc.name);
|
|
773
|
+
if ('malformed' in parsed) {
|
|
774
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, parsed.detail ?? parsed.reason, {
|
|
775
|
+
detail: parsed.reason,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (parsed.frontmatter.name !== loc.name) {
|
|
779
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'frontmatter.name cannot be changed via raw edit', { cause: 'name-rename-not-allowed' });
|
|
780
|
+
}
|
|
781
|
+
nextText = body.raw;
|
|
782
|
+
}
|
|
783
|
+
// Story 30.1 (AC4.b): block writes to git-tracked agent files when the
|
|
784
|
+
// body or frontmatter contains a plaintext secret. The hard block
|
|
785
|
+
// overrides any client-side acknowledgement — server is the last
|
|
786
|
+
// line of defence per AC4.b.
|
|
787
|
+
if (editableScope === 'project') {
|
|
788
|
+
const secrets = detectSecretsInText(nextText);
|
|
789
|
+
await assertNoSecretOnShared({
|
|
790
|
+
scope: 'project',
|
|
791
|
+
projectSlug: loc.projectSlug,
|
|
792
|
+
relativePath: `.claude/agents/${loc.name}.md`,
|
|
793
|
+
secretDetected: secrets.matched,
|
|
794
|
+
detectedAt: { lines: secrets.lines },
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
const written = await harnessService.write(ref, {
|
|
798
|
+
content: nextText,
|
|
799
|
+
expectedMtime: current.mtime,
|
|
800
|
+
});
|
|
801
|
+
const { body: nextBody } = splitFrontmatterAndBody(nextText);
|
|
802
|
+
const finalParsed = parseAgentFrontmatter(splitFrontmatterAndBody(nextText).frontmatterRaw, loc.name);
|
|
803
|
+
let toolsState = 'omitted';
|
|
804
|
+
if (!('malformed' in finalParsed)) {
|
|
805
|
+
toolsState = finalParsed.toolsState;
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
success: true,
|
|
809
|
+
mtime: written.mtime,
|
|
810
|
+
toolsState,
|
|
811
|
+
hasExampleBlock: detectExampleBlock(nextBody),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
async copyAgent(req) {
|
|
815
|
+
validateAgentName(req.sourceName);
|
|
816
|
+
const targetName = req.targetName ?? req.sourceName;
|
|
817
|
+
validateAgentName(targetName);
|
|
818
|
+
const sourceResolved = await resolveAbsoluteFile(req.sourceScope, req.sourceName, req.sourceProjectSlug, req.sourcePluginKey);
|
|
819
|
+
const sourceFile = await readAndParseAgent(sourceResolved.abs);
|
|
820
|
+
if ('malformed' in sourceFile) {
|
|
821
|
+
throwMapped(HARNESS_ERRORS.HARNESS_AGENT_NOT_FOUND.code, 'source agent not found');
|
|
822
|
+
}
|
|
823
|
+
const secrets = detectSecretsInText(sourceFile.raw);
|
|
824
|
+
if (secrets.matched && req.acknowledgedSecret !== true) {
|
|
825
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'client must show the secret modal and echo acknowledgedSecret', {
|
|
826
|
+
cause: 'secret-not-acknowledged',
|
|
827
|
+
details: { secretLines: secrets.lines },
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
const { abs: targetAbs } = await resolveAbsoluteFile(req.targetScope, targetName, req.targetProjectSlug);
|
|
831
|
+
let exists = false;
|
|
832
|
+
try {
|
|
833
|
+
const stat = await fs.stat(targetAbs);
|
|
834
|
+
if (stat.isFile())
|
|
835
|
+
exists = true;
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
// missing
|
|
839
|
+
}
|
|
840
|
+
const warnings = [];
|
|
841
|
+
if (req.sourceScope === 'plugin' && PLUGIN_ROOT_RE.test(sourceFile.raw)) {
|
|
842
|
+
warnings.push('plugin-root-reference');
|
|
843
|
+
}
|
|
844
|
+
if (exists) {
|
|
845
|
+
if (req.onConflict === 'skip') {
|
|
846
|
+
return {
|
|
847
|
+
success: true,
|
|
848
|
+
target: makeSourceLocation(req.targetScope, targetAbs, targetName, req.targetProjectSlug),
|
|
849
|
+
skipped: true,
|
|
850
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
if (req.onConflict === 'rename') {
|
|
854
|
+
if (!req.targetName || req.targetName === req.sourceName) {
|
|
855
|
+
throwMapped(HARNESS_ERRORS.HARNESS_AGENT_NAME_CONFLICT.code, 'rename requires distinct targetName');
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// overwrite → fall through
|
|
859
|
+
}
|
|
860
|
+
// Rewrite frontmatter.name to match the target file stem (key invariant).
|
|
861
|
+
const rewritten = serializeAgentFrontmatter(sourceFile.body, {
|
|
862
|
+
frontmatter: { ...sourceFile.frontmatter, name: targetName },
|
|
863
|
+
toolsState: sourceFile.toolsState,
|
|
864
|
+
});
|
|
865
|
+
await fs.mkdir(path.dirname(targetAbs), { recursive: true });
|
|
866
|
+
const targetRef = await this.buildEditableRef(req.targetScope, targetName, req.targetProjectSlug);
|
|
867
|
+
await harnessService.write(targetRef, { content: rewritten });
|
|
868
|
+
return {
|
|
869
|
+
success: true,
|
|
870
|
+
target: makeSourceLocation(req.targetScope, targetAbs, targetName, req.targetProjectSlug),
|
|
871
|
+
skipped: false,
|
|
872
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async deleteAgent(req) {
|
|
876
|
+
validateAgentName(req.name);
|
|
877
|
+
const { abs } = await resolveAbsoluteFile(req.scope, req.name, req.projectSlug);
|
|
878
|
+
let stat;
|
|
879
|
+
try {
|
|
880
|
+
stat = await fs.stat(abs);
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
if (err.code === 'ENOENT') {
|
|
884
|
+
throwMapped(HARNESS_ERRORS.HARNESS_AGENT_NOT_FOUND.code, 'agent not found');
|
|
885
|
+
}
|
|
886
|
+
throw err;
|
|
887
|
+
}
|
|
888
|
+
if (req.expectedMtime !== undefined && req.expectedMtime !== stat.mtime.toISOString()) {
|
|
889
|
+
throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
|
|
890
|
+
currentMtime: stat.mtime.toISOString(),
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
await fs.unlink(abs);
|
|
894
|
+
return { success: true };
|
|
895
|
+
}
|
|
896
|
+
// -------------------------------------------------------------------------
|
|
897
|
+
// Helpers
|
|
898
|
+
// -------------------------------------------------------------------------
|
|
899
|
+
async buildEditableRef(scope, name, projectSlug) {
|
|
900
|
+
if (scope === 'project') {
|
|
901
|
+
if (!projectSlug) {
|
|
902
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for scope=project');
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
scope: 'project',
|
|
906
|
+
projectSlug,
|
|
907
|
+
relativePath: `${AGENTS_DIR}/${name}.md`,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
scope: 'user',
|
|
912
|
+
relativePath: `${AGENTS_DIR}/${name}.md`,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// ---------------------------------------------------------------------------
|
|
917
|
+
// resolveSourceLocation — used by the controller to validate name/scope tuple
|
|
918
|
+
// ---------------------------------------------------------------------------
|
|
919
|
+
export async function resolveAgentSourceLocation(input) {
|
|
920
|
+
const { abs } = await resolveAbsoluteFile(input.scope, input.name, input.projectSlug, input.pluginKey);
|
|
921
|
+
return makeSourceLocation(input.scope, abs, input.name, input.projectSlug, input.pluginKey);
|
|
922
|
+
}
|
|
923
|
+
// Exposed for unit tests.
|
|
924
|
+
export const harnessAgentInternals = {
|
|
925
|
+
detectExampleBlock,
|
|
926
|
+
detectSecretsInText,
|
|
927
|
+
parseAgentFrontmatter,
|
|
928
|
+
serializeAgentFrontmatter,
|
|
929
|
+
validateAgentName,
|
|
930
|
+
AGENT_NAME_RE,
|
|
931
|
+
};
|
|
932
|
+
export const harnessAgentService = new HarnessAgentService();
|
|
933
|
+
//# sourceMappingURL=harnessAgentService.js.map
|