salmon-loop 0.2.3 → 0.2.16

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.
Files changed (234) hide show
  1. package/dist/cli/argv/headless-detection.js +27 -0
  2. package/dist/cli/chat-flow.js +11 -0
  3. package/dist/cli/chat.js +161 -24
  4. package/dist/cli/commands/chat.js +30 -24
  5. package/dist/cli/commands/context.js +15 -3
  6. package/dist/cli/commands/flow-mode.js +63 -0
  7. package/dist/cli/commands/help-format.js +12 -0
  8. package/dist/cli/commands/registry.js +6 -7
  9. package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
  10. package/dist/cli/commands/run/config-resolution.js +30 -24
  11. package/dist/cli/commands/run/early-errors.js +23 -0
  12. package/dist/cli/commands/run/handler.js +131 -44
  13. package/dist/cli/commands/run/headless-error-writer.js +8 -0
  14. package/dist/cli/commands/run/loop-params.js +3 -0
  15. package/dist/cli/commands/run/mode.js +2 -5
  16. package/dist/cli/commands/run/parse-options.js +18 -2
  17. package/dist/cli/commands/run/persist-session.js +10 -1
  18. package/dist/cli/commands/run/preflight.js +10 -0
  19. package/dist/cli/commands/run/reporter-factory.js +4 -0
  20. package/dist/cli/commands/run/runtime-llm.js +38 -11
  21. package/dist/cli/commands/run/runtime-options.js +2 -2
  22. package/dist/cli/commands/run/validate-options.js +0 -5
  23. package/dist/cli/commands/run/verbose.js +2 -7
  24. package/dist/cli/commands/serve.js +117 -90
  25. package/dist/cli/commands/tool-names.js +78 -78
  26. package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
  27. package/dist/cli/headless/json-protocol.js +37 -0
  28. package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
  29. package/dist/cli/headless/protocol-metadata.js +22 -0
  30. package/dist/cli/headless/stream-json-protocol.js +34 -1
  31. package/dist/cli/index.js +6 -4
  32. package/dist/cli/locales/en.js +32 -6
  33. package/dist/cli/program-bootstrap.js +14 -4
  34. package/dist/cli/program-commands.js +9 -1
  35. package/dist/cli/program-options.js +1 -0
  36. package/dist/cli/reporters/anthropic-stream.js +7 -1
  37. package/dist/cli/reporters/json.js +4 -0
  38. package/dist/cli/reporters/stream-json.js +17 -2
  39. package/dist/cli/run-cli.js +5 -3
  40. package/dist/cli/slash/runtime.js +30 -15
  41. package/dist/cli/ui/components/CommandInput.js +7 -3
  42. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  43. package/dist/cli/utils/command-option-source.js +13 -0
  44. package/dist/cli/utils/output-format.js +6 -0
  45. package/dist/cli/utils/resolve-cli-config.js +98 -0
  46. package/dist/cli/utils/verbose-level.js +8 -0
  47. package/dist/cli/utils/verify-resolver.js +8 -4
  48. package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
  49. package/dist/core/adapters/fs/file-adapter.js +6 -0
  50. package/dist/core/adapters/fs/filesystem.js +2 -1
  51. package/dist/core/adapters/git/git-adapter.js +78 -1
  52. package/dist/core/benchmark/patch-artifact.js +124 -0
  53. package/dist/core/benchmark/swe-bench.js +25 -0
  54. package/dist/core/config/load.js +39 -18
  55. package/dist/core/config/merge.js +27 -0
  56. package/dist/core/config/paths.js +24 -5
  57. package/dist/core/config/resolve-llm.js +12 -0
  58. package/dist/core/config/resolve.js +7 -5
  59. package/dist/core/config/resolvers/server.js +0 -6
  60. package/dist/core/config/validate.js +94 -21
  61. package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
  62. package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
  63. package/dist/core/context/keywords.js +18 -4
  64. package/dist/core/context/service-deps.js +2 -2
  65. package/dist/core/context/service.js +8 -0
  66. package/dist/core/context/steps/context-gather.js +38 -0
  67. package/dist/core/context/summarization/summarizer.js +55 -12
  68. package/dist/core/context/targeting/target-resolver.js +4 -4
  69. package/dist/core/extensions/index.js +23 -5
  70. package/dist/core/extensions/paths.js +31 -0
  71. package/dist/core/extensions/schemas.js +8 -5
  72. package/dist/core/facades/cli-chat.js +6 -2
  73. package/dist/core/facades/cli-command-chat.js +2 -1
  74. package/dist/core/facades/cli-command-tool-names.js +2 -0
  75. package/dist/core/facades/cli-context.js +1 -0
  76. package/dist/core/facades/cli-observability.js +1 -1
  77. package/dist/core/facades/cli-run-handler.js +4 -2
  78. package/dist/core/facades/cli-run-persist-session.js +1 -0
  79. package/dist/core/facades/cli-serve.js +2 -4
  80. package/dist/core/facades/cli-utils-worktree.js +1 -1
  81. package/dist/core/failure/diagnostics.js +53 -1
  82. package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
  83. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
  84. package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
  85. package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
  86. package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
  87. package/dist/core/grizzco/engine/transaction/transaction-runner.js +173 -7
  88. package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
  89. package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
  90. package/dist/core/grizzco/steps/answer.js +13 -14
  91. package/dist/core/grizzco/steps/autopilot.js +396 -0
  92. package/dist/core/grizzco/steps/cache-sharing.js +29 -0
  93. package/dist/core/grizzco/steps/explore.js +37 -21
  94. package/dist/core/grizzco/steps/generateReview.js +2 -5
  95. package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
  96. package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
  97. package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
  98. package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
  99. package/dist/core/grizzco/steps/patch.js +105 -146
  100. package/dist/core/grizzco/steps/plan.js +101 -25
  101. package/dist/core/grizzco/steps/preflight.js +5 -3
  102. package/dist/core/grizzco/steps/request-assembly.js +78 -0
  103. package/dist/core/grizzco/steps/research.js +39 -36
  104. package/dist/core/grizzco/steps/tool-runtime.js +47 -0
  105. package/dist/core/grizzco/steps/verify-shared.js +23 -0
  106. package/dist/core/grizzco/steps/verify.js +13 -21
  107. package/dist/core/intent/chat-intent.js +0 -4
  108. package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
  109. package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
  110. package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
  111. package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
  112. package/dist/core/llm/ai-sdk/request-params.js +74 -1
  113. package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
  114. package/dist/core/llm/ai-sdk.js +112 -27
  115. package/dist/core/llm/capabilities.js +12 -0
  116. package/dist/core/llm/contracts/repair.js +36 -30
  117. package/dist/core/llm/errors.js +83 -2
  118. package/dist/core/llm/message-composition.js +7 -22
  119. package/dist/core/llm/phase-router.js +29 -10
  120. package/dist/core/llm/redact.js +28 -3
  121. package/dist/core/llm/registry.js +2 -0
  122. package/dist/core/llm/request-augmentation.js +55 -0
  123. package/dist/core/llm/request-envelope.js +334 -0
  124. package/dist/core/llm/shared-request-assembly.js +35 -0
  125. package/dist/core/llm/stream-utils.js +13 -4
  126. package/dist/core/llm/utils.js +18 -29
  127. package/dist/core/memory/relevant-retrieval.js +144 -0
  128. package/dist/core/observability/logger.js +11 -2
  129. package/dist/core/patch/diff.js +1 -0
  130. package/dist/core/prompts/registry.js +39 -2
  131. package/dist/core/prompts/runtime.js +50 -12
  132. package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
  133. package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
  134. package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
  135. package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
  136. package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
  137. package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
  138. package/dist/core/prompts/templates/system/main_system.hbs +4 -16
  139. package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
  140. package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
  141. package/dist/core/prompts/templates/system/research_system.hbs +2 -0
  142. package/dist/core/protocols/a2a/agent-card.js +3 -2
  143. package/dist/core/protocols/a2a/sdk/executor.js +8 -6
  144. package/dist/core/protocols/a2a/sdk/server.js +0 -1
  145. package/dist/core/protocols/acp/formal-agent.js +221 -55
  146. package/dist/core/protocols/acp/handlers.js +5 -1
  147. package/dist/core/protocols/acp/permission-provider.js +21 -1
  148. package/dist/core/protocols/shared/execution-request.js +24 -0
  149. package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
  150. package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
  151. package/dist/core/public-capabilities/projections.js +29 -0
  152. package/dist/core/public-capabilities/registry.js +26 -0
  153. package/dist/core/public-capabilities/types.js +2 -0
  154. package/dist/core/runtime/agent-server-runtime.js +47 -43
  155. package/dist/core/runtime/execution-profile.js +67 -0
  156. package/dist/core/session/artifact-state.js +160 -0
  157. package/dist/core/session/compaction/index.js +183 -0
  158. package/dist/core/session/compaction/microcompact.js +78 -0
  159. package/dist/core/session/compaction/tracking.js +48 -0
  160. package/dist/core/session/compaction/types.js +11 -0
  161. package/dist/core/session/compression.js +12 -4
  162. package/dist/core/session/manager.js +247 -10
  163. package/dist/core/session/pruning-strategy.js +55 -9
  164. package/dist/core/session/replacement-preview-provider.js +24 -0
  165. package/dist/core/session/replacement-state.js +131 -0
  166. package/dist/core/session/resume-repair/pipeline.js +79 -0
  167. package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
  168. package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
  169. package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
  170. package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
  171. package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
  172. package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
  173. package/dist/core/session/resume-repair/types.js +2 -0
  174. package/dist/core/session/summary-sync.js +164 -13
  175. package/dist/core/session/token-tracker.js +6 -0
  176. package/dist/core/skills/audit.js +34 -0
  177. package/dist/core/skills/bridge.js +84 -7
  178. package/dist/core/skills/discovery.js +94 -0
  179. package/dist/core/skills/feature-flags.js +52 -0
  180. package/dist/core/skills/index.js +1 -1
  181. package/dist/core/skills/loader.js +195 -20
  182. package/dist/core/skills/parser.js +296 -24
  183. package/dist/core/skills/permissions.js +117 -0
  184. package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
  185. package/dist/core/skills/runtime/SkillRunner.js +240 -61
  186. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
  187. package/dist/core/strata/layers/worktree.js +70 -13
  188. package/dist/core/strata/runtime/synchronizer.js +29 -2
  189. package/dist/core/streaming/stream-assembler.js +75 -31
  190. package/dist/core/sub-agent/context-snapshot.js +156 -0
  191. package/dist/core/sub-agent/core/loop.js +1 -1
  192. package/dist/core/sub-agent/core/manager.js +119 -20
  193. package/dist/core/sub-agent/dispatch-policy.js +29 -0
  194. package/dist/core/sub-agent/prefix-consistency.js +48 -0
  195. package/dist/core/sub-agent/registry-defaults.js +4 -0
  196. package/dist/core/sub-agent/tools/task-spawn.js +79 -2
  197. package/dist/core/sub-agent/types.js +134 -5
  198. package/dist/core/tools/audit.js +13 -4
  199. package/dist/core/tools/builtin/ast-grep.js +1 -1
  200. package/dist/core/tools/builtin/ast.js +1 -1
  201. package/dist/core/tools/builtin/benchmark.js +360 -0
  202. package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
  203. package/dist/core/tools/builtin/code-search/executor.js +6 -1
  204. package/dist/core/tools/builtin/code-search/spec.js +26 -2
  205. package/dist/core/tools/builtin/fs.js +256 -23
  206. package/dist/core/tools/builtin/git.js +2 -2
  207. package/dist/core/tools/builtin/index.js +51 -2
  208. package/dist/core/tools/builtin/interaction.js +8 -1
  209. package/dist/core/tools/builtin/plan.js +37 -15
  210. package/dist/core/tools/builtin/shell.js +1 -1
  211. package/dist/core/tools/loader.js +39 -16
  212. package/dist/core/tools/mapper.js +17 -3
  213. package/dist/core/tools/parallel/scheduler.js +35 -4
  214. package/dist/core/tools/permissions/permission-rules.js +5 -10
  215. package/dist/core/tools/policy.js +6 -1
  216. package/dist/core/tools/recoverable-tool-errors.js +10 -0
  217. package/dist/core/tools/router.js +24 -6
  218. package/dist/core/tools/session.js +458 -48
  219. package/dist/core/tools/tool-visibility.js +62 -0
  220. package/dist/core/tools/types.js +9 -1
  221. package/dist/core/types/execution.js +4 -0
  222. package/dist/core/types/flow-mode.js +8 -0
  223. package/dist/core/utils/path.js +52 -0
  224. package/dist/core/verification/runner.js +4 -1
  225. package/dist/interfaces/cli/task-runner.js +4 -3
  226. package/dist/languages/typescript/index.js +4 -1
  227. package/dist/locales/en.js +87 -2
  228. package/dist/utils/eol.js +1 -1
  229. package/package.json +15 -8
  230. package/scripts/fix-es-abstract-compat.js +77 -0
  231. package/dist/core/runtime/fastify-server-bundle.js +0 -26
  232. package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
  233. package/dist/core/runtime/sidecar-paths.js +0 -47
  234. package/dist/core/runtime/sidecar-route-catalog.js +0 -103
@@ -1,14 +1,57 @@
1
1
  import { z } from 'zod';
2
- import { MicroTaskRunner } from './runtime/MicroTaskRunner.js';
2
+ import { tryGetLogger } from '../observability/logger.js';
3
+ import { emitSkillAuditEvent, generateSkillTraceId, hashSkillArgs } from './audit.js';
4
+ import { getSkillFeatureFlags } from './feature-flags.js';
5
+ import { executeSkill } from './runtime/SkillRunner.js';
3
6
  /**
4
- * Bridges a Skill into a ToolSpec compatible with the standard tool registry.
7
+ * Check whether the bridge execution path kill-switch is active.
8
+ *
9
+ * Delegates to the centralized {@link getSkillFeatureFlags} module which
10
+ * reads `SALMONLOOP_DISABLE_BRIDGE_SKILL_EXEC` from the environment.
11
+ *
12
+ * - 'true' or '1' → bridge disabled (kill-switch ON)
13
+ * - 'false' or '0' → bridge enabled (kill-switch OFF)
14
+ * - not set → disabled in non-dev, enabled in development
15
+ *
16
+ * @see Requirements 9.4, 11.4
5
17
  */
6
- export function skillToToolSpec(skill) {
18
+ export function isBridgeSkillExecDisabled() {
19
+ return getSkillFeatureFlags().bridgeDisabled;
20
+ }
21
+ function isLazySource(source) {
22
+ return 'entry' in source && 'loader' in source;
23
+ }
24
+ /**
25
+ * Bridges a Skill (or catalog entry) into a ToolSpec compatible with the
26
+ * standard tool registry.
27
+ *
28
+ * When given a catalog entry + loader, the executor performs Tier 2 activation
29
+ * on first invocation (progressive disclosure). When given a full Skill, it
30
+ * executes immediately.
31
+ *
32
+ * The executor delegates to executeSkill() which routes all shell commands
33
+ * through ToolRouter governance (Registry → Validation → Policy → Auth).
34
+ *
35
+ * When the kill-switch env var `SALMONLOOP_DISABLE_BRIDGE_SKILL_EXEC` is
36
+ * set to 'true' or '1', the executor returns a DENIED result and emits
37
+ * a SKILL_EXECUTION_DENIED audit event instead of executing the skill.
38
+ *
39
+ * @param source - Either a full {@link Skill} or a `{ entry, loader }` pair
40
+ * for lazy activation.
41
+ * @param routerBox - A mutable box whose `.router` field will be populated
42
+ * after the ToolRouter is created. The executor reads it lazily at call
43
+ * time, so it is safe to pass an initially-null box.
44
+ */
45
+ export function skillToToolSpec(source, routerBox) {
46
+ const skillId = isLazySource(source) ? source.entry.id : source.id;
47
+ const description = isLazySource(source) ? source.entry.description : source.metadata.description;
48
+ // Cache for lazily activated skill (Tier 2)
49
+ let activatedSkill = isLazySource(source) ? null : source;
7
50
  return {
8
- name: skill.id,
51
+ name: skillId,
9
52
  source: 'plugin',
10
53
  intent: 'AGENT',
11
- description: skill.metadata.description,
54
+ description,
12
55
  riskLevel: 'medium',
13
56
  sideEffects: ['process', 'fs_read'],
14
57
  concurrency: 'serial_only',
@@ -21,8 +64,42 @@ export function skillToToolSpec(skill) {
21
64
  status: z.string(),
22
65
  }),
23
66
  executor: async (input, ctx) => {
24
- const runner = new MicroTaskRunner(skill);
25
- const result = await runner.execute({ args: input.args || '' }, ctx);
67
+ if (isBridgeSkillExecDisabled()) {
68
+ const traceId = generateSkillTraceId(skillId);
69
+ const argsHash = hashSkillArgs(input.args || '');
70
+ emitSkillAuditEvent({
71
+ type: 'SKILL_EXECUTION_DENIED',
72
+ skillId,
73
+ route: 'tool-bridge',
74
+ runnerClass: 'MicroTaskRunner',
75
+ commandCount: 0,
76
+ authorizationMode: 'blocking',
77
+ argsHash,
78
+ traceId,
79
+ denyReason: 'BRIDGE_KILL_SWITCH',
80
+ denySource: 'env:SALMONLOOP_DISABLE_BRIDGE_SKILL_EXEC',
81
+ });
82
+ const logger = tryGetLogger();
83
+ logger?.warn(`Bridge skill execution denied by kill-switch for skill "${skillId}" (traceId=${traceId})`);
84
+ return { prompt: '', status: 'DENIED' };
85
+ }
86
+ // Tier 2 activation: load full skill content on first invocation
87
+ if (!activatedSkill) {
88
+ const lazySource = source;
89
+ activatedSkill = await lazySource.loader.activateSkill(lazySource.entry.id);
90
+ }
91
+ // Lazily read the router from the box — it is populated after filtering.
92
+ const toolRouter = routerBox.router;
93
+ if (!toolRouter) {
94
+ throw new Error(`ToolRouter not yet initialized for skill "${skillId}"`);
95
+ }
96
+ const result = await executeSkill({
97
+ skill: activatedSkill,
98
+ argsText: input.args || '',
99
+ toolRouter,
100
+ toolCtx: ctx,
101
+ route: 'tool-bridge',
102
+ });
26
103
  return {
27
104
  prompt: result.injectedPrompt,
28
105
  status: result.status,
@@ -0,0 +1,94 @@
1
+ import { text } from '../../locales/index.js';
2
+ import { getLogger } from '../observability/logger.js';
3
+ /**
4
+ * Check whether a file path matches a glob-like pattern.
5
+ *
6
+ * Supports:
7
+ * - `**` to match any number of path segments
8
+ * - `*` to match any characters within a single path segment
9
+ * - Literal path matching
10
+ *
11
+ * @param filePath - The file path to test (forward-slash normalized)
12
+ * @param pattern - The glob pattern to match against
13
+ * @returns true if the file path matches the pattern
14
+ */
15
+ export function matchGlob(filePath, pattern) {
16
+ // Normalize separators to forward slash for consistent matching
17
+ const normalizedPath = filePath.replace(/\\/g, '/');
18
+ const normalizedPattern = pattern.replace(/\\/g, '/');
19
+ // Build regex from glob pattern character by character
20
+ let regexStr = '';
21
+ let i = 0;
22
+ while (i < normalizedPattern.length) {
23
+ const ch = normalizedPattern[i];
24
+ if (ch === '*' && normalizedPattern[i + 1] === '*') {
25
+ // ** — match any number of path segments (including zero)
26
+ // Consume optional trailing slash: **/ matches zero-or-more dirs
27
+ i += 2;
28
+ if (normalizedPattern[i] === '/') {
29
+ i++;
30
+ // `**/` matches zero or more directory segments
31
+ regexStr += '(?:.+/)?';
32
+ }
33
+ else {
34
+ // `**` at end matches everything
35
+ regexStr += '.*';
36
+ }
37
+ }
38
+ else if (ch === '*') {
39
+ // * — match any characters except /
40
+ regexStr += '[^/]*';
41
+ i++;
42
+ }
43
+ else {
44
+ // Escape regex special characters for literal match
45
+ regexStr += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
46
+ i++;
47
+ }
48
+ }
49
+ const regex = new RegExp(`^${regexStr}$`);
50
+ return regex.test(normalizedPath);
51
+ }
52
+ /**
53
+ * Signal-based skill discovery watcher.
54
+ *
55
+ * Does NOT use fs.watch — instead provides methods that can be called
56
+ * when file operations happen. The caller handles the event source.
57
+ *
58
+ * Supports:
59
+ * - Re-scanning search paths for newly added skill directories (Requirement 7.1)
60
+ *
61
+ * @see Requirements 7.1, 7.2
62
+ */
63
+ export class SkillDiscoveryWatcher {
64
+ /** Known skill ids from the last catalog snapshot. */
65
+ knownIds = new Set();
66
+ constructor(initialCatalog) {
67
+ for (const entry of initialCatalog) {
68
+ this.knownIds.add(entry.id);
69
+ }
70
+ }
71
+ /**
72
+ * Accept a refreshed catalog and return entries that are new
73
+ * (not previously known).
74
+ *
75
+ * Call this after re-scanning search paths (e.g. via SkillLoader.loadCatalog())
76
+ * when a file-operation signal indicates new skill directories may exist.
77
+ *
78
+ * @param refreshedCatalog - The full catalog from a fresh scan
79
+ * @returns Newly discovered catalog entries not in the previous snapshot
80
+ * @see Requirement 7.1
81
+ */
82
+ refreshCatalog(refreshedCatalog) {
83
+ const newEntries = [];
84
+ for (const entry of refreshedCatalog) {
85
+ if (!this.knownIds.has(entry.id)) {
86
+ newEntries.push(entry);
87
+ this.knownIds.add(entry.id);
88
+ getLogger().info(text.skills.newSkillDiscovered(entry.id, entry.location));
89
+ }
90
+ }
91
+ return newEntries;
92
+ }
93
+ }
94
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Centralized feature flags for the skills subsystem.
3
+ *
4
+ * Provides rollout control over bridge execution.
5
+ *
6
+ * @see Requirements 11.4
7
+ */
8
+ import { tryGetLogger } from '../observability/logger.js';
9
+ // ---------------------------------------------------------------------------
10
+ // Env-var helpers
11
+ // ---------------------------------------------------------------------------
12
+ /**
13
+ * Parse a boolean-ish env var value.
14
+ * Returns `true` for 'true'/'1', `false` for 'false'/'0', or the
15
+ * provided `fallback` when the value is undefined/empty.
16
+ */
17
+ function parseBoolEnv(value, fallback) {
18
+ if (value === 'true' || value === '1')
19
+ return true;
20
+ if (value === 'false' || value === '0')
21
+ return false;
22
+ return fallback;
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // Public API
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Read all skill-related feature flags from environment variables.
29
+ *
30
+ * The function is intentionally pure (no caching) so that tests can
31
+ * manipulate `process.env` between calls and observe the effect.
32
+ *
33
+ * @returns A snapshot of the current feature flag values
34
+ * @see Requirements 11.4
35
+ */
36
+ export function getSkillFeatureFlags() {
37
+ const bridgeDefault = process.env.NODE_ENV !== 'development';
38
+ return {
39
+ bridgeDisabled: parseBoolEnv(process.env.SALMONLOOP_DISABLE_BRIDGE_SKILL_EXEC, bridgeDefault),
40
+ };
41
+ }
42
+ /**
43
+ * Log the current feature flag values at debug level.
44
+ *
45
+ * Useful at startup to record which flags are active for diagnostics.
46
+ */
47
+ export function logSkillFeatureFlags() {
48
+ const flags = getSkillFeatureFlags();
49
+ const logger = tryGetLogger();
50
+ logger?.debug(`Skill feature flags: bridgeDisabled=${flags.bridgeDisabled}`);
51
+ }
52
+ //# sourceMappingURL=feature-flags.js.map
@@ -3,6 +3,6 @@ export * from './loader.js';
3
3
  export * from './parser.js';
4
4
  export * from './strategy.js';
5
5
  export * from './bridge.js';
6
- export { MicroTaskRunner } from './runtime/MicroTaskRunner.js';
6
+ export * from './feature-flags.js';
7
7
  export * from './runtime/SkillRunner.js';
8
8
  //# sourceMappingURL=index.js.map
@@ -1,71 +1,246 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
+ import { text } from '../../locales/index.js';
3
4
  import { syncFs as fs } from '../adapters/fs/node-fs.js';
4
- import { getLogger } from '../observability/logger.js';
5
+ import { tryGetLogger } from '../observability/logger.js';
5
6
  import { SkillParser } from './parser.js';
7
+ /**
8
+ * Safe logger accessor that never throws when the logger is not yet initialized.
9
+ *
10
+ * Falls back to a no-op stub so that loader code can run in test environments
11
+ * or early startup paths where the global logger has not been set.
12
+ */
13
+ function safeLogger() {
14
+ return (tryGetLogger() ?? {
15
+ error: (..._args) => { },
16
+ warn: (..._args) => { },
17
+ info: (..._args) => { },
18
+ debug: (..._args) => { },
19
+ audit: (..._args) => { },
20
+ });
21
+ }
6
22
  export class SkillLoader {
7
23
  options;
24
+ /**
25
+ * Format the skill catalog into a model-consumable disclosure block.
26
+ *
27
+ * Pure data contract: accepts catalog + optional context paths, returns string.
28
+ * No dependency on session or prompt layer.
29
+ *
30
+ * @param catalog - Array of SkillCatalogEntry from loadCatalog()
31
+ * @returns Formatted string with preamble + skill entries, or empty string if
32
+ * no disclosable skills
33
+ * @see Requirements 4.1, 4.6
34
+ */
35
+ static formatCatalogDisclosure(catalog) {
36
+ if (catalog.length === 0) {
37
+ return '';
38
+ }
39
+ const preamble = `## Available Skills\n\n${text.skills.catalogDisclosurePreamble}`;
40
+ const entries = catalog
41
+ .map((entry) => `- **${entry.name}**: ${entry.description}\n Location: ${entry.location}`)
42
+ .join('\n\n');
43
+ return `${preamble}\n\n${entries}\n`;
44
+ }
45
+ /** Cache of fully activated skills (Tier 2). */
46
+ activated = new Map();
47
+ /** Cached catalog entries from the last loadCatalog() call. */
48
+ catalogCache = null;
8
49
  constructor(options) {
9
50
  this.options = options;
10
51
  }
11
- async initialize() {
12
- const inventory = [];
52
+ /**
53
+ * Tier 2: Activate a skill by id — load full SKILL.md content on demand.
54
+ *
55
+ * Looks up the skill in the catalog (loading it first if necessary),
56
+ * reads the full file content, parses it with {@link SkillParser.parse},
57
+ * and caches the result. Subsequent calls for the same id return the
58
+ * cached {@link Skill} without re-reading the file.
59
+ *
60
+ * @param id - The skill identifier (must match a catalog entry)
61
+ * @returns The fully loaded {@link Skill} with instructions
62
+ * @throws Error if the skill id is not found in the catalog
63
+ * @see Requirements 6.2, 6.4
64
+ */
65
+ async activateSkill(id) {
66
+ // Return cached activation if already loaded
67
+ const cached = this.activated.get(id);
68
+ if (cached) {
69
+ return cached;
70
+ }
71
+ // Ensure catalog is available
72
+ if (!this.catalogCache) {
73
+ this.catalogCache = await this.loadCatalog();
74
+ }
75
+ const entry = this.catalogCache.find((e) => e.id === id);
76
+ if (!entry) {
77
+ throw new Error(text.skills.skillNotFoundInCatalog(id));
78
+ }
79
+ // Read full content and parse with SkillParser
80
+ const content = fs.readFileSync(entry.location, 'utf-8');
81
+ const skill = SkillParser.parse(content, entry.location);
82
+ this.activated.set(id, skill);
83
+ safeLogger().info(text.skills.skillActivated(id));
84
+ return skill;
85
+ }
86
+ /**
87
+ * Tier 1: Load a lightweight catalog of all discoverable skills.
88
+ *
89
+ * Parses only YAML frontmatter (name + description + location) without
90
+ * loading full instruction content. Keeps startup context cost at
91
+ * approximately 50-100 tokens per skill.
92
+ *
93
+ * Uses the same search path priority and conflict resolution as
94
+ * {@link initialize}, but avoids reading instruction bodies.
95
+ *
96
+ * @returns Array of {@link SkillCatalogEntry} in discovery priority order
97
+ * @see Requirements 6.1, 6.3
98
+ */
99
+ async loadCatalog() {
100
+ const catalog = [];
13
101
  const seen = new Map();
14
102
  for (const target of this.buildSearchPaths()) {
15
103
  if (!fs.existsSync(target.path))
16
104
  continue;
105
+ const scope = this.labelToScope(target.label);
17
106
  const entries = fs.readdirSync(target.path, { withFileTypes: true });
18
107
  for (const entry of entries) {
19
- let skillFile = null;
20
- if (entry.isDirectory()) {
21
- skillFile = path.join(target.path, entry.name, 'SKILL.md');
108
+ if (!entry.isDirectory())
109
+ continue;
110
+ const skillFile = path.join(target.path, entry.name, 'SKILL.md');
111
+ if (!skillFile || !fs.existsSync(skillFile))
112
+ continue;
113
+ try {
114
+ const content = fs.readFileSync(skillFile, 'utf-8');
115
+ const catalogEntry = SkillParser.parseFrontmatterOnly(content, skillFile, scope);
116
+ if (seen.has(catalogEntry.id)) {
117
+ const firstSource = seen.get(catalogEntry.id);
118
+ safeLogger().warn(`Duplicate skill ${catalogEntry.id} found in ${skillFile}; already loaded from ${firstSource}`);
119
+ safeLogger().audit('SKILL_DUPLICATE_SKIPPED', {
120
+ skillId: catalogEntry.id,
121
+ skippedPath: skillFile,
122
+ firstSource,
123
+ reason: 'first_win_conflict_resolution',
124
+ }, { source: 'skill-loader', severity: 'low', scope: 'repo' });
125
+ continue;
126
+ }
127
+ seen.set(catalogEntry.id, `${target.label}:${skillFile}`);
128
+ catalog.push(catalogEntry);
22
129
  }
23
- else if (entry.name.endsWith('.md')) {
24
- skillFile = path.join(target.path, entry.name);
130
+ catch (err) {
131
+ safeLogger().error(`Failed to load skill catalog entry at ${skillFile}: ${err instanceof Error ? err.message : String(err)}`);
25
132
  }
133
+ }
134
+ }
135
+ this.catalogCache = catalog;
136
+ return catalog;
137
+ }
138
+ async initialize() {
139
+ return this.loadSkillsFromPaths();
140
+ }
141
+ /**
142
+ * Synchronous variant of {@link initialize} for callers that cannot await.
143
+ *
144
+ * Safe because all underlying I/O uses `syncFs` (synchronous fs operations).
145
+ * Used by tool-name discovery for tab completion.
146
+ */
147
+ initializeSync() {
148
+ return this.loadSkillsFromPaths();
149
+ }
150
+ loadSkillsFromPaths() {
151
+ const inventory = [];
152
+ const seen = new Map();
153
+ for (const target of this.buildSearchPaths()) {
154
+ if (!fs.existsSync(target.path))
155
+ continue;
156
+ const entries = fs.readdirSync(target.path, { withFileTypes: true });
157
+ for (const entry of entries) {
158
+ if (!entry.isDirectory())
159
+ continue;
160
+ const skillFile = path.join(target.path, entry.name, 'SKILL.md');
26
161
  if (!skillFile || !fs.existsSync(skillFile))
27
162
  continue;
28
163
  try {
29
164
  const content = fs.readFileSync(skillFile, 'utf-8');
30
165
  const skill = SkillParser.parse(content, skillFile);
31
166
  if (seen.has(skill.id)) {
32
- getLogger().warn(`Duplicate skill ${skill.id} found in ${skillFile}; already loaded from ${seen.get(skill.id)}`);
167
+ const firstSource = seen.get(skill.id);
168
+ safeLogger().warn(`Duplicate skill ${skill.id} found in ${skillFile}; already loaded from ${firstSource}`);
169
+ safeLogger().audit('SKILL_DUPLICATE_SKIPPED', {
170
+ skillId: skill.id,
171
+ skippedPath: skillFile,
172
+ firstSource,
173
+ reason: 'first_win_conflict_resolution',
174
+ }, { source: 'skill-loader', severity: 'low', scope: 'repo' });
33
175
  continue;
34
176
  }
35
177
  seen.set(skill.id, `${target.label}:${skillFile}`);
36
178
  inventory.push(skill);
37
179
  }
38
180
  catch (err) {
39
- getLogger().error(`Failed to load skill at ${skillFile}: ${err instanceof Error ? err.message : String(err)}`);
181
+ safeLogger().error(`Failed to load skill at ${skillFile}: ${err instanceof Error ? err.message : String(err)}`);
40
182
  }
41
183
  }
42
184
  }
43
185
  return inventory;
44
186
  }
187
+ /**
188
+ * Derive the catalog scope from a search path label.
189
+ *
190
+ * Labels follow the pattern `prefix:path` where prefix is one of:
191
+ * - `config` → 'config' scope (extra paths from skills.json)
192
+ * - `repo` → 'repo' scope (repo-level directories)
193
+ * - `user` → 'user' scope (user home directories)
194
+ * - `repo` / others → 'repo' scope
195
+ */
196
+ labelToScope(label) {
197
+ if (label.startsWith('config:'))
198
+ return 'config';
199
+ if (label.startsWith('user:'))
200
+ return 'user';
201
+ return 'repo';
202
+ }
203
+ /**
204
+ * Build the ordered list of skill search paths.
205
+ *
206
+ * Priority (high → low):
207
+ * 1. Config extra paths (from skills.json discovery.paths)
208
+ * 2. {repoRoot}/.salmonloop/skills
209
+ * 3. {repoRoot}/.agents/skills (cross-client interop)
210
+ * 4. ~/.salmonloop/skills
211
+ * 5. ~/.agents/skills (cross-client interop)
212
+ *
213
+ * First-win conflict resolution: when two skills share the same name,
214
+ * the one from the higher-priority path wins and a warning is logged
215
+ * (handled in initialize()).
216
+ */
45
217
  buildSearchPaths() {
46
218
  const paths = [];
219
+ // 1. Config extra paths (highest priority)
47
220
  const extra = this.options.extraPaths ?? [];
48
221
  for (const extraPath of extra) {
49
222
  paths.push({ path: extraPath, label: `config:${extraPath}` });
50
223
  }
224
+ // 2. Repo-level .salmonloop/skills
51
225
  paths.push({
52
226
  path: path.join(this.options.repoRoot, '.salmonloop', 'skills'),
53
227
  label: 'repo:.salmonloop/skills',
54
228
  });
229
+ // 3. Repo-level .agents/skills (cross-client interop)
230
+ paths.push({
231
+ path: path.join(this.options.repoRoot, '.agents', 'skills'),
232
+ label: 'repo:.agents/skills',
233
+ });
234
+ // 4. User-level ~/.salmonloop/skills
55
235
  paths.push({
56
236
  path: path.join(os.homedir(), '.salmonloop', 'skills'),
57
237
  label: 'user:~/.salmonloop/skills',
58
238
  });
59
- if (this.options.useDefaults ?? true) {
60
- paths.push({
61
- path: path.join(os.homedir(), '.claude', 'skills'),
62
- label: 'compat:~/.claude/skills',
63
- });
64
- paths.push({
65
- path: path.join(this.options.repoRoot, '.claude', 'skills'),
66
- label: 'compat:.claude/skills',
67
- });
68
- }
239
+ // 5. User-level ~/.agents/skills (cross-client interop)
240
+ paths.push({
241
+ path: path.join(os.homedir(), '.agents', 'skills'),
242
+ label: 'user:~/.agents/skills',
243
+ });
69
244
  const deduped = [];
70
245
  const seen = new Set();
71
246
  for (const entry of paths) {