salmon-loop 0.2.13 → 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 (218) 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 +160 -24
  4. package/dist/cli/commands/chat.js +14 -7
  5. package/dist/cli/commands/flow-mode.js +63 -0
  6. package/dist/cli/commands/registry.js +2 -0
  7. package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
  8. package/dist/cli/commands/run/early-errors.js +23 -0
  9. package/dist/cli/commands/run/handler.js +115 -27
  10. package/dist/cli/commands/run/headless-error-writer.js +8 -0
  11. package/dist/cli/commands/run/loop-params.js +2 -0
  12. package/dist/cli/commands/run/mode.js +2 -5
  13. package/dist/cli/commands/run/parse-options.js +16 -0
  14. package/dist/cli/commands/run/persist-session.js +10 -1
  15. package/dist/cli/commands/run/preflight.js +10 -0
  16. package/dist/cli/commands/run/reporter-factory.js +4 -0
  17. package/dist/cli/commands/run/runtime-llm.js +38 -11
  18. package/dist/cli/commands/run/runtime-options.js +2 -2
  19. package/dist/cli/commands/serve.js +91 -71
  20. package/dist/cli/commands/tool-names.js +78 -78
  21. package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
  22. package/dist/cli/headless/json-protocol.js +37 -0
  23. package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
  24. package/dist/cli/headless/protocol-metadata.js +22 -0
  25. package/dist/cli/headless/stream-json-protocol.js +34 -1
  26. package/dist/cli/index.js +6 -4
  27. package/dist/cli/locales/en.js +30 -6
  28. package/dist/cli/program-bootstrap.js +8 -3
  29. package/dist/cli/program-commands.js +5 -1
  30. package/dist/cli/reporters/anthropic-stream.js +7 -1
  31. package/dist/cli/reporters/json.js +4 -0
  32. package/dist/cli/reporters/stream-json.js +17 -2
  33. package/dist/cli/run-cli.js +5 -3
  34. package/dist/cli/slash/runtime.js +27 -12
  35. package/dist/cli/ui/components/CommandInput.js +7 -3
  36. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  37. package/dist/cli/utils/command-option-source.js +13 -0
  38. package/dist/cli/utils/verify-resolver.js +8 -4
  39. package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
  40. package/dist/core/adapters/fs/file-adapter.js +6 -0
  41. package/dist/core/adapters/fs/filesystem.js +2 -1
  42. package/dist/core/adapters/git/git-adapter.js +78 -1
  43. package/dist/core/benchmark/patch-artifact.js +124 -0
  44. package/dist/core/benchmark/swe-bench.js +25 -0
  45. package/dist/core/config/load.js +18 -11
  46. package/dist/core/config/resolve-llm.js +12 -0
  47. package/dist/core/config/resolvers/server.js +0 -6
  48. package/dist/core/config/validate.js +73 -21
  49. package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
  50. package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
  51. package/dist/core/context/keywords.js +18 -4
  52. package/dist/core/context/service-deps.js +2 -2
  53. package/dist/core/context/service.js +8 -0
  54. package/dist/core/context/steps/context-gather.js +38 -0
  55. package/dist/core/context/summarization/summarizer.js +55 -12
  56. package/dist/core/context/targeting/target-resolver.js +4 -4
  57. package/dist/core/extensions/index.js +23 -5
  58. package/dist/core/extensions/paths.js +31 -0
  59. package/dist/core/extensions/schemas.js +8 -5
  60. package/dist/core/facades/cli-chat.js +6 -2
  61. package/dist/core/facades/cli-command-chat.js +1 -0
  62. package/dist/core/facades/cli-command-tool-names.js +2 -0
  63. package/dist/core/facades/cli-observability.js +1 -1
  64. package/dist/core/facades/cli-run-handler.js +4 -2
  65. package/dist/core/facades/cli-run-persist-session.js +1 -0
  66. package/dist/core/facades/cli-serve.js +2 -4
  67. package/dist/core/facades/cli-utils-worktree.js +1 -1
  68. package/dist/core/failure/diagnostics.js +53 -1
  69. package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
  70. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
  71. package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
  72. package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
  73. package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
  74. package/dist/core/grizzco/engine/transaction/transaction-runner.js +165 -7
  75. package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
  76. package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
  77. package/dist/core/grizzco/steps/answer.js +13 -14
  78. package/dist/core/grizzco/steps/autopilot.js +396 -0
  79. package/dist/core/grizzco/steps/cache-sharing.js +29 -0
  80. package/dist/core/grizzco/steps/explore.js +37 -21
  81. package/dist/core/grizzco/steps/generateReview.js +2 -5
  82. package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
  83. package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
  84. package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
  85. package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
  86. package/dist/core/grizzco/steps/patch.js +105 -146
  87. package/dist/core/grizzco/steps/plan.js +101 -25
  88. package/dist/core/grizzco/steps/preflight.js +5 -6
  89. package/dist/core/grizzco/steps/request-assembly.js +78 -0
  90. package/dist/core/grizzco/steps/research.js +39 -36
  91. package/dist/core/grizzco/steps/tool-runtime.js +47 -0
  92. package/dist/core/grizzco/steps/verify-shared.js +23 -0
  93. package/dist/core/grizzco/steps/verify.js +13 -21
  94. package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
  95. package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
  96. package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
  97. package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
  98. package/dist/core/llm/ai-sdk/request-params.js +73 -0
  99. package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
  100. package/dist/core/llm/ai-sdk.js +112 -27
  101. package/dist/core/llm/capabilities.js +12 -0
  102. package/dist/core/llm/contracts/repair.js +36 -30
  103. package/dist/core/llm/errors.js +83 -2
  104. package/dist/core/llm/message-composition.js +7 -22
  105. package/dist/core/llm/phase-router.js +29 -10
  106. package/dist/core/llm/redact.js +28 -3
  107. package/dist/core/llm/registry.js +2 -0
  108. package/dist/core/llm/request-augmentation.js +55 -0
  109. package/dist/core/llm/request-envelope.js +334 -0
  110. package/dist/core/llm/shared-request-assembly.js +35 -0
  111. package/dist/core/llm/stream-utils.js +13 -4
  112. package/dist/core/llm/utils.js +18 -29
  113. package/dist/core/memory/relevant-retrieval.js +144 -0
  114. package/dist/core/observability/logger.js +11 -2
  115. package/dist/core/patch/diff.js +1 -0
  116. package/dist/core/prompts/registry.js +39 -2
  117. package/dist/core/prompts/runtime.js +50 -12
  118. package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
  119. package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
  120. package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
  121. package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
  122. package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
  123. package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
  124. package/dist/core/prompts/templates/system/main_system.hbs +4 -16
  125. package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
  126. package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
  127. package/dist/core/prompts/templates/system/research_system.hbs +2 -0
  128. package/dist/core/protocols/a2a/agent-card.js +3 -2
  129. package/dist/core/protocols/a2a/sdk/executor.js +2 -1
  130. package/dist/core/protocols/a2a/sdk/server.js +0 -1
  131. package/dist/core/protocols/acp/formal-agent.js +74 -51
  132. package/dist/core/protocols/acp/handlers.js +5 -1
  133. package/dist/core/protocols/acp/permission-provider.js +1 -1
  134. package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
  135. package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
  136. package/dist/core/public-capabilities/projections.js +29 -0
  137. package/dist/core/public-capabilities/registry.js +26 -0
  138. package/dist/core/public-capabilities/types.js +2 -0
  139. package/dist/core/runtime/agent-server-runtime.js +47 -43
  140. package/dist/core/runtime/execution-profile.js +67 -0
  141. package/dist/core/session/artifact-state.js +160 -0
  142. package/dist/core/session/compaction/index.js +183 -0
  143. package/dist/core/session/compaction/microcompact.js +78 -0
  144. package/dist/core/session/compaction/tracking.js +48 -0
  145. package/dist/core/session/compaction/types.js +11 -0
  146. package/dist/core/session/compression.js +8 -0
  147. package/dist/core/session/manager.js +244 -8
  148. package/dist/core/session/pruning-strategy.js +55 -9
  149. package/dist/core/session/replacement-preview-provider.js +24 -0
  150. package/dist/core/session/replacement-state.js +131 -0
  151. package/dist/core/session/resume-repair/pipeline.js +79 -0
  152. package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
  153. package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
  154. package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
  155. package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
  156. package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
  157. package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
  158. package/dist/core/session/resume-repair/types.js +2 -0
  159. package/dist/core/session/summary-sync.js +164 -13
  160. package/dist/core/session/token-tracker.js +6 -0
  161. package/dist/core/skills/audit.js +34 -0
  162. package/dist/core/skills/bridge.js +84 -7
  163. package/dist/core/skills/discovery.js +94 -0
  164. package/dist/core/skills/feature-flags.js +52 -0
  165. package/dist/core/skills/index.js +1 -1
  166. package/dist/core/skills/loader.js +195 -20
  167. package/dist/core/skills/parser.js +296 -24
  168. package/dist/core/skills/permissions.js +117 -0
  169. package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
  170. package/dist/core/skills/runtime/SkillRunner.js +240 -61
  171. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
  172. package/dist/core/strata/layers/worktree.js +67 -10
  173. package/dist/core/strata/runtime/synchronizer.js +29 -2
  174. package/dist/core/streaming/stream-assembler.js +75 -31
  175. package/dist/core/sub-agent/context-snapshot.js +156 -0
  176. package/dist/core/sub-agent/core/loop.js +1 -1
  177. package/dist/core/sub-agent/core/manager.js +119 -20
  178. package/dist/core/sub-agent/dispatch-policy.js +29 -0
  179. package/dist/core/sub-agent/prefix-consistency.js +48 -0
  180. package/dist/core/sub-agent/registry-defaults.js +4 -0
  181. package/dist/core/sub-agent/tools/task-spawn.js +79 -2
  182. package/dist/core/sub-agent/types.js +134 -5
  183. package/dist/core/tools/audit.js +13 -4
  184. package/dist/core/tools/builtin/ast-grep.js +1 -1
  185. package/dist/core/tools/builtin/ast.js +1 -1
  186. package/dist/core/tools/builtin/benchmark.js +360 -0
  187. package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
  188. package/dist/core/tools/builtin/code-search/executor.js +6 -1
  189. package/dist/core/tools/builtin/code-search/spec.js +26 -2
  190. package/dist/core/tools/builtin/fs.js +256 -23
  191. package/dist/core/tools/builtin/git.js +2 -2
  192. package/dist/core/tools/builtin/index.js +51 -2
  193. package/dist/core/tools/builtin/interaction.js +8 -1
  194. package/dist/core/tools/builtin/plan.js +37 -15
  195. package/dist/core/tools/builtin/shell.js +1 -1
  196. package/dist/core/tools/loader.js +39 -16
  197. package/dist/core/tools/mapper.js +17 -3
  198. package/dist/core/tools/parallel/scheduler.js +35 -4
  199. package/dist/core/tools/permissions/permission-rules.js +5 -10
  200. package/dist/core/tools/policy.js +6 -1
  201. package/dist/core/tools/recoverable-tool-errors.js +10 -0
  202. package/dist/core/tools/router.js +24 -6
  203. package/dist/core/tools/session.js +458 -48
  204. package/dist/core/tools/tool-visibility.js +62 -0
  205. package/dist/core/tools/types.js +9 -1
  206. package/dist/core/types/execution.js +4 -0
  207. package/dist/core/types/flow-mode.js +8 -0
  208. package/dist/core/utils/path.js +52 -0
  209. package/dist/core/verification/runner.js +4 -1
  210. package/dist/languages/typescript/index.js +4 -1
  211. package/dist/locales/en.js +35 -2
  212. package/dist/utils/eol.js +1 -1
  213. package/package.json +13 -6
  214. package/scripts/fix-es-abstract-compat.js +77 -0
  215. package/dist/core/runtime/fastify-server-bundle.js +0 -26
  216. package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
  217. package/dist/core/runtime/sidecar-paths.js +0 -47
  218. package/dist/core/runtime/sidecar-route-catalog.js +0 -103
@@ -0,0 +1,8 @@
1
+ import { normalizeSessionArtifactState } from '../../artifact-state.js';
2
+ import { normalizeToolResultReplacementState } from '../../replacement-state.js';
3
+ export const reattachRuntimeStateStage = async (state) => {
4
+ state.session.meta.artifactState = normalizeSessionArtifactState(state.session.meta.artifactState);
5
+ state.replacementState = normalizeToolResultReplacementState(state.replacementState);
6
+ state.session.meta.replacementState = state.replacementState;
7
+ };
8
+ //# sourceMappingURL=reattach-runtime-state.js.map
@@ -0,0 +1,10 @@
1
+ export const recoverOrphanedBranchesStage = async (state) => {
2
+ if (state.session.iterations.length > state.session.meta.totalIterations) {
3
+ state.session.meta.totalIterations = state.session.iterations.length;
4
+ state.repairActions.push({
5
+ code: 'RECOVERED_ITERATION_COUNT',
6
+ detail: 'Normalized stale totalIterations from recovered iteration list.',
7
+ });
8
+ }
9
+ };
10
+ //# sourceMappingURL=recover-orphaned-branches.js.map
@@ -0,0 +1,36 @@
1
+ function hasFiniteTimestamp(value) {
2
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
3
+ }
4
+ export const relinkBoundaryAndTailStage = async (state) => {
5
+ const meta = state.session.meta;
6
+ if (!meta.id ||
7
+ !meta.name ||
8
+ !hasFiniteTimestamp(meta.createdAt) ||
9
+ !hasFiniteTimestamp(meta.updatedAt)) {
10
+ state.contractViolations.push({
11
+ code: 'MALFORMED_SESSION_BOUNDARY_METADATA',
12
+ message: 'Archive metadata failed boundary validation.',
13
+ });
14
+ return;
15
+ }
16
+ const invalidMessage = state.session.messages.some((message) => !hasFiniteTimestamp(message.timestamp) || !message.id);
17
+ if (invalidMessage) {
18
+ state.contractViolations.push({
19
+ code: 'MALFORMED_MESSAGE_BOUNDARY_METADATA',
20
+ message: 'Recovered messages contain invalid boundary metadata.',
21
+ });
22
+ return;
23
+ }
24
+ const invalidIteration = state.session.iterations.some((iteration) => typeof iteration.id !== 'string' ||
25
+ !iteration.id ||
26
+ !Number.isInteger(iteration.attempt) ||
27
+ iteration.attempt <= 0 ||
28
+ typeof iteration.contextSummary !== 'string');
29
+ if (invalidIteration) {
30
+ state.contractViolations.push({
31
+ code: 'MALFORMED_TAIL_ITERATION_METADATA',
32
+ message: 'Recovered iterations contain invalid tail metadata.',
33
+ });
34
+ }
35
+ };
36
+ //# sourceMappingURL=relink-boundary-and-tail.js.map
@@ -0,0 +1,23 @@
1
+ export const replayStartupHooksStage = async (state, context) => {
2
+ const executed = new Set();
3
+ for (const hook of context.startupHooks) {
4
+ if (!hook?.key || executed.has(hook.key))
5
+ continue;
6
+ executed.add(hook.key);
7
+ try {
8
+ await hook.run(state.session, {
9
+ now: context.now,
10
+ nextId: context.nextId,
11
+ });
12
+ }
13
+ catch (error) {
14
+ const reason = error instanceof Error ? error.message : String(error);
15
+ state.contractViolations.push({
16
+ code: 'STARTUP_HOOK_FAILED',
17
+ message: `Startup hook "${hook.key}" failed: ${reason}`,
18
+ });
19
+ return;
20
+ }
21
+ }
22
+ };
23
+ //# sourceMappingURL=replay-startup-hooks.js.map
@@ -0,0 +1,17 @@
1
+ export const rescueStaleMetadataStage = async (state, context) => {
2
+ if (!state.session.meta.name.trim()) {
3
+ state.session.meta.name = `Recovered ${state.session.meta.id}`;
4
+ state.repairActions.push({
5
+ code: 'RESCUED_EMPTY_NAME',
6
+ detail: 'Recovered missing session name from archive metadata.',
7
+ });
8
+ }
9
+ if (state.session.meta.updatedAt < state.session.meta.createdAt) {
10
+ state.session.meta.updatedAt = context.now();
11
+ state.repairActions.push({
12
+ code: 'RESCUED_UPDATED_AT',
13
+ detail: 'Adjusted stale updatedAt to a valid timestamp.',
14
+ });
15
+ }
16
+ };
17
+ //# sourceMappingURL=rescue-stale-metadata.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -1,11 +1,100 @@
1
1
  import { ConversationSummarizer } from '../context/summarization/summarizer.js';
2
2
  import { DEFAULT_SUMMARIZATION_CONFIG } from '../context/summarization/types.js';
3
+ import { microcompact } from './compaction/microcompact.js';
4
+ import { buildSessionConversationContext } from './session-context-builder.js';
3
5
  import { TokenTracker } from './token-tracker.js';
6
+ const MAX_RECOVERY_READ_FILES = 6;
7
+ const MAX_RECOVERY_SAFE_HINT_CHARS = 240;
8
+ function trimToUndefined(value) {
9
+ if (typeof value !== 'string')
10
+ return undefined;
11
+ const trimmed = value.trim();
12
+ return trimmed ? trimmed : undefined;
13
+ }
14
+ function clampText(value, maxChars) {
15
+ if (!value)
16
+ return undefined;
17
+ if (value.length <= maxChars)
18
+ return value;
19
+ return `${value.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;
20
+ }
21
+ function normalizeFailureSummary(value) {
22
+ if (!value)
23
+ return undefined;
24
+ const next = {};
25
+ const reasonCode = trimToUndefined(value.reasonCode);
26
+ if (reasonCode)
27
+ next.reasonCode = reasonCode;
28
+ const diagnosticCode = trimToUndefined(value.diagnosticCode);
29
+ if (diagnosticCode)
30
+ next.diagnosticCode = diagnosticCode;
31
+ const safeHint = clampText(trimToUndefined(value.safeHint), MAX_RECOVERY_SAFE_HINT_CHARS);
32
+ if (safeHint)
33
+ next.safeHint = safeHint;
34
+ const failurePhase = trimToUndefined(value.failurePhase);
35
+ if (failurePhase)
36
+ next.failurePhase = failurePhase;
37
+ return Object.keys(next).length > 0 ? next : undefined;
38
+ }
39
+ function normalizeRecentReadFiles(value) {
40
+ if (!Array.isArray(value) || value.length === 0)
41
+ return undefined;
42
+ const unique = [];
43
+ const seen = new Set();
44
+ for (const entry of value) {
45
+ const path = trimToUndefined(typeof entry === 'string' ? entry : entry?.path);
46
+ if (!path || seen.has(path))
47
+ continue;
48
+ seen.add(path);
49
+ unique.push(path);
50
+ }
51
+ if (unique.length === 0)
52
+ return undefined;
53
+ return unique.slice(-MAX_RECOVERY_READ_FILES);
54
+ }
55
+ function buildRecoveryState(params) {
56
+ const { sessionManager, persistedState, patch } = params;
57
+ const next = {};
58
+ const flowMode = sessionManager.getChatFlowMode?.() ?? persistedState?.recoveryState?.flowMode;
59
+ if (flowMode) {
60
+ next.flowMode = flowMode;
61
+ }
62
+ const recentReadFiles = normalizeRecentReadFiles(sessionManager.getArtifactState?.()?.recentReadArtifacts ??
63
+ persistedState?.recoveryState?.recentReadFiles);
64
+ if (recentReadFiles?.length) {
65
+ next.recentReadFiles = recentReadFiles;
66
+ }
67
+ const lastFailureSummary = patch && 'lastFailureSummary' in patch
68
+ ? normalizeFailureSummary(patch.lastFailureSummary)
69
+ : normalizeFailureSummary(persistedState?.recoveryState?.lastFailureSummary);
70
+ if (lastFailureSummary) {
71
+ next.lastFailureSummary = lastFailureSummary;
72
+ }
73
+ return Object.keys(next).length > 0 ? next : undefined;
74
+ }
75
+ function buildRecoveryStateMessage(summaryState) {
76
+ const recoveryState = summaryState?.recoveryState;
77
+ if (!recoveryState)
78
+ return undefined;
79
+ return {
80
+ role: 'system',
81
+ content: `[Conversation recovery state]\n${JSON.stringify(recoveryState)}`,
82
+ };
83
+ }
84
+ function fitRecoveryStateMessage(params) {
85
+ const { message, budgetTokens, countTokens } = params;
86
+ if (!message)
87
+ return undefined;
88
+ if (budgetTokens <= 0)
89
+ return undefined;
90
+ const tokens = Math.max(0, Math.floor(countTokens(message.content)));
91
+ return tokens <= budgetTokens ? message : undefined;
92
+ }
4
93
  export async function refreshSessionSummary(params) {
5
94
  const { sessionManager, llm, contextHash } = params;
6
95
  const strategy = params.strategy ?? 'auto';
7
96
  if (!sessionManager)
8
- return;
97
+ return { didSummarize: false };
9
98
  try {
10
99
  const summarizer = new ConversationSummarizer({
11
100
  chat: async ({ messages, temperature, maxTokens }) => {
@@ -32,33 +121,58 @@ export async function refreshSessionSummary(params) {
32
121
  if (persistedState) {
33
122
  summarizer.restoreState(persistedState);
34
123
  }
35
- const messages = sessionManager.getMessagesWithIds().map((msg) => ({
36
- id: msg.id,
124
+ const rawMessages = sessionManager.getMessagesWithIds();
125
+ const messages = microcompact(rawMessages).map((msg, index) => ({
126
+ id: msg.id ?? rawMessages[index]?.id ?? `msg-${index}-${msg.timestamp}`,
37
127
  role: msg.role,
38
128
  content: msg.content,
39
129
  timestamp: msg.timestamp,
40
130
  }));
131
+ let didSummarize = false;
41
132
  if (strategy === 'force') {
42
- await summarizer.forceSummarize(messages, contextHash);
133
+ const result = await summarizer.forceSummarize(messages, contextHash);
134
+ didSummarize = Boolean(result);
43
135
  }
44
136
  else {
45
- await summarizer.triggerSummarization(messages, contextHash);
137
+ const result = await summarizer.triggerSummarization(messages, contextHash);
138
+ didSummarize = Boolean(result);
46
139
  }
47
- sessionManager.updateSummaryState(summarizer.getState());
140
+ const nextState = summarizer.getState();
141
+ nextState.recoveryState = buildRecoveryState({
142
+ sessionManager,
143
+ persistedState,
144
+ patch: params.recoveryStatePatch,
145
+ });
146
+ sessionManager.updateSummaryState(nextState);
147
+ return { didSummarize };
48
148
  }
49
- catch {
50
- // Best-effort summary update: never affect execution flow.
149
+ catch (error) {
150
+ if (params.strict) {
151
+ throw error;
152
+ }
153
+ return { didSummarize: false, error: error instanceof Error ? error.message : String(error) };
51
154
  }
52
155
  }
53
156
  export function buildEffectiveConversationContext(params) {
54
157
  const { sessionManager } = params;
55
158
  const summaryState = sessionManager.getSummaryState();
56
- const messages = sessionManager.getMessagesWithIds().map((msg) => ({
57
- id: msg.id,
159
+ const countTokens = params.countTokens ?? ((text) => TokenTracker.estimateTokens(text));
160
+ // Apply microcompact (Level 0) to all messages before building context
161
+ // This is a "view-only" operation that doesn't modify sessionManager history
162
+ const rawMessages = params.messages ?? sessionManager.getMessages();
163
+ const messages = microcompact(rawMessages).map((msg, index) => ({
164
+ id: msg.id ?? `msg-${index}-${msg.timestamp}`,
58
165
  role: msg.role,
59
166
  content: msg.content,
60
167
  timestamp: msg.timestamp,
61
168
  }));
169
+ if (!summaryState) {
170
+ return buildSessionConversationContext(messages, {
171
+ budgetTokens: params.budgetTokens ?? Number.MAX_SAFE_INTEGER,
172
+ maxMessages: params.maxMessages,
173
+ countTokens,
174
+ });
175
+ }
62
176
  const summarizer = new ConversationSummarizer({
63
177
  chat: async () => ({ content: '' }),
64
178
  }, {
@@ -75,8 +189,45 @@ export function buildEffectiveConversationContext(params) {
75
189
  if (summaryState) {
76
190
  summarizer.restoreState(summaryState);
77
191
  }
78
- return summarizer
79
- .getEffectiveContext(messages)
80
- .map((m) => ({ role: m.role, content: m.content }));
192
+ const effective = summarizer.getEffectiveContext(messages);
193
+ const recentMessages = effective
194
+ .filter((message) => message.role === 'user' || message.role === 'assistant')
195
+ .map((message) => ({
196
+ id: message.id,
197
+ role: message.role,
198
+ content: message.content,
199
+ timestamp: message.timestamp,
200
+ }));
201
+ const recoveryMessage = fitRecoveryStateMessage({
202
+ message: buildRecoveryStateMessage(summaryState),
203
+ budgetTokens: params.budgetTokens ?? Number.MAX_SAFE_INTEGER,
204
+ countTokens,
205
+ });
206
+ const recoveryBudget = recoveryMessage && params.budgetTokens !== undefined
207
+ ? Math.max(0, params.budgetTokens - Math.max(0, Math.floor(countTokens(recoveryMessage.content))))
208
+ : params.budgetTokens;
209
+ const summaryStateForContext = summaryState.recoveryState === undefined
210
+ ? summaryState
211
+ : {
212
+ ...summaryState,
213
+ recoveryState: undefined,
214
+ };
215
+ const built = buildSessionConversationContext(recentMessages, {
216
+ budgetTokens: recoveryBudget ?? Number.MAX_SAFE_INTEGER,
217
+ maxMessages: params.maxMessages,
218
+ countTokens,
219
+ summaryState: summaryStateForContext,
220
+ });
221
+ if (!recoveryMessage)
222
+ return built;
223
+ let systemPrefixLength = 0;
224
+ while (systemPrefixLength < built.length && built[systemPrefixLength]?.role === 'system') {
225
+ systemPrefixLength += 1;
226
+ }
227
+ return [
228
+ ...built.slice(0, systemPrefixLength),
229
+ recoveryMessage,
230
+ ...built.slice(systemPrefixLength),
231
+ ];
81
232
  }
82
233
  //# sourceMappingURL=summary-sync.js.map
@@ -78,5 +78,11 @@ export class TokenTracker {
78
78
  static estimateTokens(text) {
79
79
  return Math.ceil(text.length / 4);
80
80
  }
81
+ /**
82
+ * Estimate total tokens for a list of messages
83
+ */
84
+ static estimateMessagesTokens(messages) {
85
+ return messages.reduce((sum, m) => sum + this.estimateTokens(m.content), 0);
86
+ }
81
87
  }
82
88
  //# sourceMappingURL=token-tracker.js.map
@@ -0,0 +1,34 @@
1
+ import * as crypto from 'crypto';
2
+ import { tryGetLogger } from '../observability/logger.js';
3
+ /**
4
+ * Compute a stable SHA-256 hash (truncated to 16 hex chars) of the given arguments text.
5
+ */
6
+ export function hashSkillArgs(argsText) {
7
+ if (!argsText)
8
+ return undefined;
9
+ return crypto.createHash('sha256').update(argsText).digest('hex').slice(0, 16);
10
+ }
11
+ /**
12
+ * Generate a unique trace ID for a skill execution.
13
+ */
14
+ export function generateSkillTraceId(skillId) {
15
+ return `skill-${skillId}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
16
+ }
17
+ /**
18
+ * Emit a structured skill audit event via the logger audit trail.
19
+ *
20
+ * Uses `logger.audit()` to ensure events are persisted in the audit trail
21
+ * with appropriate severity and source metadata.
22
+ */
23
+ export function emitSkillAuditEvent(event) {
24
+ const logger = tryGetLogger();
25
+ if (!logger)
26
+ return;
27
+ const severity = event.type === 'SKILL_EXECUTION_DENIED' ? 'high' : 'low';
28
+ logger.audit(event.type, event, {
29
+ source: 'skill-executor',
30
+ severity,
31
+ scope: 'session',
32
+ });
33
+ }
34
+ //# sourceMappingURL=audit.js.map
@@ -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