salmon-loop 0.3.2 → 0.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.
Files changed (227) hide show
  1. package/dist/cli/authorization/non-interactive.js +9 -13
  2. package/dist/cli/authorization/provider.js +2 -10
  3. package/dist/cli/chat.js +12 -6
  4. package/dist/cli/commands/allowlist.js +1 -1
  5. package/dist/cli/commands/chat.js +13 -13
  6. package/dist/cli/commands/config.js +2 -2
  7. package/dist/cli/commands/mode.js +2 -2
  8. package/dist/cli/commands/parallel.js +1 -1
  9. package/dist/cli/commands/run/handler.js +9 -4
  10. package/dist/cli/commands/run/loop-params.js +2 -0
  11. package/dist/cli/commands/run/parse-options.js +14 -26
  12. package/dist/cli/commands/run/runtime-llm.js +15 -12
  13. package/dist/cli/commands/run/runtime-options.js +3 -1
  14. package/dist/cli/config.js +0 -8
  15. package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
  16. package/dist/cli/locales/en.js +2 -2
  17. package/dist/cli/reporters/standard.js +12 -3
  18. package/dist/cli/reporters/stream-json.js +2 -1
  19. package/dist/cli/slash/runtime.js +2 -2
  20. package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
  21. package/dist/cli/ui/hooks/useLoopState.js +1 -1
  22. package/dist/core/adapters/fs/file-adapter.js +3 -1
  23. package/dist/core/adapters/git/git-adapter.js +6 -3
  24. package/dist/core/adapters/git/git-runner.js +5 -2
  25. package/dist/core/adapters/git/lock-manager.js +7 -4
  26. package/dist/core/ast/parser.js +18 -9
  27. package/dist/core/checkpoint-domain/manifest-store.js +21 -13
  28. package/dist/core/checkpoint-domain/service.js +3 -1
  29. package/dist/core/config/limits.js +1 -1
  30. package/dist/core/config/model-pricing.js +61 -0
  31. package/dist/core/config/schema.js +738 -0
  32. package/dist/core/config/validate.js +11 -922
  33. package/dist/core/context/ast/skeleton-extractor.js +225 -0
  34. package/dist/core/context/ast/source-outline.js +24 -1
  35. package/dist/core/context/budget/dynamic-adjuster.js +20 -5
  36. package/dist/core/context/builder.js +7 -3
  37. package/dist/core/context/cache/store-factory.js +3 -1
  38. package/dist/core/context/dependencies.js +2 -1
  39. package/dist/core/context/effectiveness/persistence.js +50 -0
  40. package/dist/core/context/effectiveness/tracker.js +24 -0
  41. package/dist/core/context/gatherers/architecture-gatherer.js +2 -1
  42. package/dist/core/context/gatherers/artifact-gatherer.js +7 -4
  43. package/dist/core/context/gatherers/ast-gatherer.js +34 -40
  44. package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
  45. package/dist/core/context/gatherers/git-history-gatherer.js +3 -1
  46. package/dist/core/context/gatherers/knowledge-gatherer.js +21 -2
  47. package/dist/core/context/gatherers/metadata-gatherer.js +12 -7
  48. package/dist/core/context/gatherers/ripgrep-gatherer.js +6 -3
  49. package/dist/core/context/service.js +12 -2
  50. package/dist/core/context/steps/context-gather.js +14 -3
  51. package/dist/core/context/steps/context-targets.js +1 -0
  52. package/dist/core/context/targeting/target-resolver.js +29 -11
  53. package/dist/core/context/token/cache.js +5 -2
  54. package/dist/core/context/token/encoding-registry.js +7 -6
  55. package/dist/core/context/truncation/strategies/json.js +5 -2
  56. package/dist/core/context/truncation/type-detector.js +3 -1
  57. package/dist/core/extensions/index.js +48 -3
  58. package/dist/core/extensions/load.js +3 -2
  59. package/dist/core/extensions/merge.js +5 -1
  60. package/dist/core/extensions/paths.js +8 -2
  61. package/dist/core/extensions/schemas.js +21 -0
  62. package/dist/core/facades/cli-authorization-provider.js +1 -0
  63. package/dist/core/facades/cli-command-chat.js +2 -0
  64. package/dist/core/facades/cli-run-handler.js +1 -0
  65. package/dist/core/facades/cli-utils-serialize.js +2 -0
  66. package/dist/core/feedback/parsers.js +290 -1
  67. package/dist/core/grizzco/dsl/llm-strategy.js +4 -3
  68. package/dist/core/grizzco/engine/observability/loop-telemetry.js +5 -2
  69. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +30 -13
  70. package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
  71. package/dist/core/grizzco/engine/transaction/attempt-failure.js +49 -24
  72. package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
  73. package/dist/core/grizzco/engine/transaction/transaction-runner.js +40 -34
  74. package/dist/core/grizzco/execution/RejectionManager.js +7 -5
  75. package/dist/core/grizzco/runtime/apply-back-runtime.js +5 -2
  76. package/dist/core/grizzco/services/implementations/default/GitConfigService.js +2 -1
  77. package/dist/core/grizzco/services/registry.js +18 -0
  78. package/dist/core/grizzco/steps/audit.js +20 -10
  79. package/dist/core/grizzco/steps/autopilot.js +21 -32
  80. package/dist/core/grizzco/steps/display-report.js +4 -11
  81. package/dist/core/grizzco/steps/explore.js +14 -4
  82. package/dist/core/grizzco/steps/generateReview.js +3 -1
  83. package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
  84. package/dist/core/grizzco/steps/patch.js +1 -0
  85. package/dist/core/grizzco/steps/plan.js +58 -49
  86. package/dist/core/grizzco/steps/research.js +3 -1
  87. package/dist/core/grizzco/steps/tool-runtime.js +3 -0
  88. package/dist/core/grizzco/steps/verify.js +7 -1
  89. package/dist/core/grizzco/validation/AstValidationService.js +3 -1
  90. package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
  91. package/dist/core/history/input-history.js +3 -1
  92. package/dist/core/intent/chat-intent.js +3 -1
  93. package/dist/core/llm/ai-sdk/message-mapper.js +37 -26
  94. package/dist/core/llm/ai-sdk/request-params.js +2 -6
  95. package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
  96. package/dist/core/llm/ai-sdk/retry-classifier.js +17 -7
  97. package/dist/core/llm/ai-sdk/retry-executor.js +1 -1
  98. package/dist/core/llm/contracts/repair.js +16 -8
  99. package/dist/core/llm/errors.js +18 -14
  100. package/dist/core/llm/output-policy.js +8 -0
  101. package/dist/core/llm/redact.js +1 -3
  102. package/dist/core/llm/retry-utils.js +8 -2
  103. package/dist/core/llm/stream-utils.js +5 -3
  104. package/dist/core/llm/sub-agent-factory.js +51 -0
  105. package/dist/core/llm/tool-calling-stub.js +48 -0
  106. package/dist/core/llm/utils.js +17 -6
  107. package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
  108. package/dist/core/mcp/bridge/resource-context-provider.js +3 -1
  109. package/dist/core/mcp/bridge/tool-bridge.js +5 -14
  110. package/dist/core/mcp/catalog/discovery.js +3 -1
  111. package/dist/core/mcp/client/connection-manager.js +7 -4
  112. package/dist/core/mcp/client/transport-factory.js +7 -3
  113. package/dist/core/mcp/host/sampling-provider.js +1 -1
  114. package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
  115. package/dist/core/memory/relevant-retrieval.js +6 -4
  116. package/dist/core/observability/audit-file.js +2 -1
  117. package/dist/core/observability/audit-trail.js +3 -1
  118. package/dist/core/observability/authorization-decisions.js +13 -12
  119. package/dist/core/observability/error-mapping.js +2 -1
  120. package/dist/core/observability/logger.js +2 -1
  121. package/dist/core/observability/monitor.js +24 -0
  122. package/dist/core/observability/run-outcome-reporter.js +1 -0
  123. package/dist/core/observability/token-usage.js +5 -4
  124. package/dist/core/permission-gate/default-gate.js +5 -8
  125. package/dist/core/plan/storage.js +7 -4
  126. package/dist/core/plugin/loader.js +8 -5
  127. package/dist/core/prompts/registry.js +12 -30
  128. package/dist/core/prompts/runtime.js +3 -1
  129. package/dist/core/prompts/templates/system/autopilot_system.hbs +28 -4
  130. package/dist/core/protocols/a2a/sdk/executor.js +3 -1
  131. package/dist/core/protocols/a2a/sdk/server.js +5 -4
  132. package/dist/core/protocols/acp/acp-command-runner.js +7 -6
  133. package/dist/core/protocols/acp/acp-session-persistence.js +13 -10
  134. package/dist/core/protocols/acp/formal-agent.js +13 -6
  135. package/dist/core/protocols/acp/permission-provider.js +3 -2
  136. package/dist/core/protocols/acp/stdio-server.js +6 -6
  137. package/dist/core/reflection/engine.js +114 -14
  138. package/dist/core/runtime/agent-server-runtime.js +3 -2
  139. package/dist/core/runtime/batch-runner.js +81 -0
  140. package/dist/core/runtime/initialize.js +71 -6
  141. package/dist/core/runtime/loop-finalize.js +3 -0
  142. package/dist/core/runtime/loop-session-runner.js +5 -0
  143. package/dist/core/runtime/loop.js +4 -0
  144. package/dist/core/runtime/paths.js +9 -6
  145. package/dist/core/runtime/spawn-interactive.js +5 -4
  146. package/dist/core/security/redaction.js +3 -2
  147. package/dist/core/session/compaction/index.js +4 -3
  148. package/dist/core/session/compression.js +3 -1
  149. package/dist/core/session/manager.js +26 -38
  150. package/dist/core/session/pruning-strategy.js +2 -1
  151. package/dist/core/session/token-tracker.js +27 -9
  152. package/dist/core/skills/parser.js +3 -2
  153. package/dist/core/skills/permissions.js +2 -2
  154. package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
  155. package/dist/core/skills/runtime/SkillRunner.js +5 -2
  156. package/dist/core/slash/steps/slash-execute.js +7 -5
  157. package/dist/core/slash/strategy.js +1 -1
  158. package/dist/core/strata/checkpoint/manager.js +16 -10
  159. package/dist/core/strata/checkpoint/snapshot-create.js +5 -4
  160. package/dist/core/strata/checkpoint/snapshot-write-tree.js +7 -3
  161. package/dist/core/strata/engine/shadow-merge-engine.js +4 -2
  162. package/dist/core/strata/interaction/file-system-provider.js +2 -1
  163. package/dist/core/strata/layers/file-state-resolver.js +9 -7
  164. package/dist/core/strata/layers/immutable-git-layer.js +3 -1
  165. package/dist/core/strata/layers/shadow-driver/readonly-lock.js +8 -6
  166. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +2 -1
  167. package/dist/core/strata/layers/worktree.js +9 -10
  168. package/dist/core/strata/runtime/environment.js +2 -1
  169. package/dist/core/strata/runtime/synchronizer.js +28 -26
  170. package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
  171. package/dist/core/structured-output/json-extract.js +3 -1
  172. package/dist/core/structured-output/json-schema-validator.js +1 -13
  173. package/dist/core/sub-agent/artifacts/store.js +2 -1
  174. package/dist/core/sub-agent/context-snapshot.js +12 -6
  175. package/dist/core/sub-agent/controller.js +70 -1
  176. package/dist/core/sub-agent/core/loop.js +25 -3
  177. package/dist/core/sub-agent/core/manager.js +343 -117
  178. package/dist/core/sub-agent/registry-defaults.js +12 -0
  179. package/dist/core/sub-agent/registry.js +8 -0
  180. package/dist/core/sub-agent/summary.js +96 -0
  181. package/dist/core/sub-agent/team.js +98 -0
  182. package/dist/core/sub-agent/tools/task-await.js +109 -0
  183. package/dist/core/sub-agent/tools/task-spawn.js +52 -7
  184. package/dist/core/sub-agent/tools/team.js +92 -0
  185. package/dist/core/sub-agent/types.js +11 -2
  186. package/dist/core/target-runtime/profile.js +3 -1
  187. package/dist/core/tools/audit.js +3 -2
  188. package/dist/core/tools/budget.js +7 -12
  189. package/dist/core/tools/builtin/ast.js +144 -0
  190. package/dist/core/tools/builtin/code-search/backends/powershell.js +3 -1
  191. package/dist/core/tools/builtin/code-search/backends/rg.js +3 -1
  192. package/dist/core/tools/builtin/code-search/executor.js +46 -43
  193. package/dist/core/tools/builtin/code-search/parse/plain-grep.js +3 -1
  194. package/dist/core/tools/builtin/code-search/parse/rg-json.js +3 -1
  195. package/dist/core/tools/builtin/fs.js +90 -7
  196. package/dist/core/tools/builtin/git.js +242 -0
  197. package/dist/core/tools/builtin/glob.js +79 -0
  198. package/dist/core/tools/builtin/index.js +53 -111
  199. package/dist/core/tools/builtin/interaction.js +13 -15
  200. package/dist/core/tools/builtin/knowledge.js +146 -4
  201. package/dist/core/tools/builtin/proposal.js +14 -3
  202. package/dist/core/tools/builtin/verify.js +35 -3
  203. package/dist/core/tools/capability/executor.js +5 -5
  204. package/dist/core/tools/headless-payload.js +1 -3
  205. package/dist/core/tools/mapper.js +8 -42
  206. package/dist/core/tools/parallel/persistence.js +17 -5
  207. package/dist/core/tools/parallel/scheduler.js +23 -21
  208. package/dist/core/tools/permissions/permission-rules.js +69 -115
  209. package/dist/core/tools/plugins/loader.js +4 -3
  210. package/dist/core/tools/router.js +112 -58
  211. package/dist/core/tools/session.js +64 -102
  212. package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
  213. package/dist/core/tools/tool-visibility.js +2 -1
  214. package/dist/core/tools/types.js +10 -0
  215. package/dist/core/types/batch.js +2 -0
  216. package/dist/core/utils/error.js +79 -0
  217. package/dist/core/utils/sanitizer.js +5 -2
  218. package/dist/core/utils/serialize.js +66 -0
  219. package/dist/core/utils/zod.js +29 -0
  220. package/dist/core/verification/detect-runner.js +86 -0
  221. package/dist/core/verification/runner.js +76 -0
  222. package/dist/core/version.js +3 -1
  223. package/dist/core/workspace/capabilities.js +3 -2
  224. package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
  225. package/dist/languages/python/index.js +154 -0
  226. package/dist/locales/en.js +8 -1
  227. package/package.json +2 -1
@@ -2,6 +2,7 @@ import { recordAuditEvent, setAuditContext } from '../../../observability/audit-
2
2
  import { getLogger } from '../../../observability/logger.js';
3
3
  import { appendPlanNote } from '../../../plan/index.js';
4
4
  import { EXECUTION_PHASES, } from '../../../types/runtime.js';
5
+ import { isRecord } from '../../../utils/serialize.js';
5
6
  /**
6
7
  * Typed Async Pipeline Container
7
8
  */
@@ -28,271 +29,179 @@ export class Pipeline {
28
29
  * Add a step to the pipeline
29
30
  */
30
31
  step(name, action) {
31
- const nextPromise = this.promise.then(async (ctx) => {
32
- const start = Date.now();
33
- let phaseStarted = false;
34
- let errorStr;
35
- let errorMeta;
36
- let result;
37
- const emit = ctx.emit;
38
- const isPhase = (value) => EXECUTION_PHASES.includes(value);
39
- const planRuntime = ctx?.planRuntime;
40
- const persistenceRoot = ctx?.workspace?.baseRepoPath || ctx?.workspace?.workPath;
41
- const attempt = ctx?.attempt ?? 1;
42
- const tryAppendPlanNote = async (note) => {
43
- if (!planRuntime || !persistenceRoot)
44
- return;
45
- try {
46
- await appendPlanNote({
47
- persistenceRoot,
48
- sessionId: planRuntime.sessionId,
49
- note,
50
- });
51
- recordAuditEvent('plan.runtime.note.append', { note, ok: true }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
52
- return true;
53
- }
54
- catch (e) {
55
- const msg = e instanceof Error ? e.message : String(e);
56
- recordAuditEvent('plan.runtime.note.append.failed', { note, error: msg }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
57
- getLogger().debug(`[PlanRuntime] Failed to append note: ${msg}`);
58
- return false;
59
- }
60
- };
61
- try {
62
- this.ctxRef.current = ctx;
63
- const signal = ctx?.options?.signal;
64
- const strategy = ctx?.workspace?.strategy ?? ctx?.options?.strategy;
65
- if (signal?.aborted && strategy === 'worktree') {
66
- throw new Error('Operation cancelled by user');
67
- }
68
- setAuditContext({ phase: name });
69
- if (emit && isPhase(name)) {
70
- emit({ type: 'phase.start', phase: name, timestamp: new Date() });
71
- phaseStarted = true;
72
- const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.start ${name}`);
73
- if (planRuntime && ok !== undefined) {
74
- emit({
75
- type: 'plan.runtime.journal',
76
- sessionId: planRuntime.sessionId,
77
- phase: name,
78
- kind: 'start',
79
- attempt,
80
- ok,
81
- timestamp: new Date(),
82
- });
83
- }
84
- }
85
- result = await action(ctx);
86
- this.ctxRef.current = result;
87
- // APPLY_BACK failure is represented as a structured result (not an exception) to preserve context.
88
- // The pipeline must still treat it as a phase failure for progress reporting and plan journaling.
89
- if (name === 'APPLY_BACK' &&
90
- result &&
91
- typeof result === 'object' &&
92
- result.applyBackResult &&
93
- typeof result.applyBackResult === 'object') {
94
- const applyBackResult = result.applyBackResult;
95
- if (applyBackResult.success === false && !applyBackResult.skipped) {
96
- errorStr = applyBackResult.safeMessage || applyBackResult.error || 'Apply-back failed';
97
- errorMeta = {
32
+ const nextPromise = this.executeStep(name, action, async () => {
33
+ // No recovery for plain steps — check for APPLY_BACK structured failure
34
+ const result = this.ctxRef.current;
35
+ if (name === 'APPLY_BACK' && isRecord(result) && isRecord(result.applyBackResult)) {
36
+ const applyBackResult = result.applyBackResult;
37
+ if (applyBackResult.success === false && !applyBackResult.skipped) {
38
+ return {
39
+ errorStr: applyBackResult.safeMessage || applyBackResult.error || 'Apply-back failed',
40
+ errorMeta: {
98
41
  name: 'ApplyBackFailure',
99
42
  code: applyBackResult.errorCode || 'APPLY_BACK_FAILED',
100
- };
101
- }
43
+ },
44
+ };
102
45
  }
103
- return result;
104
46
  }
105
- catch (error) {
106
- errorStr = error instanceof Error ? error.message : String(error);
107
- errorMeta =
108
- typeof error === 'object' && error !== null
109
- ? {
110
- name: error.name,
111
- code: error.code,
112
- llmCode: error.llmCode,
113
- }
114
- : undefined;
115
- throw error;
47
+ return null;
48
+ });
49
+ return new Pipeline(nextPromise, this.startTime, name, this.traces, this.ctxRef);
50
+ }
51
+ /**
52
+ * Add a step with error recovery
53
+ */
54
+ stepWithRecovery(name, action, recovery) {
55
+ const nextPromise = this.executeStep(name, action, async (ctx, errorStr) => {
56
+ // Only run recovery when the step actually failed
57
+ if (!errorStr)
58
+ return null;
59
+ const recStart = Date.now();
60
+ try {
61
+ await recovery(ctx);
62
+ this.traces.push({
63
+ name: `${name}:recovery`,
64
+ start: recStart,
65
+ end: Date.now(),
66
+ duration: Date.now() - recStart,
67
+ metadata: { success: true },
68
+ });
116
69
  }
117
- finally {
118
- if (emit && isPhase(name) && phaseStarted) {
119
- emit({
120
- type: 'phase.end',
121
- phase: name,
122
- success: !errorStr,
123
- timestamp: new Date(),
124
- });
125
- const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.end ${name} (success=${String(!errorStr)})`);
126
- if (planRuntime && ok !== undefined) {
127
- emit({
128
- type: 'plan.runtime.journal',
129
- sessionId: planRuntime.sessionId,
130
- phase: name,
131
- kind: 'end',
132
- attempt,
133
- ok,
134
- timestamp: new Date(),
135
- });
136
- }
137
- }
138
- setAuditContext({ phase: undefined });
139
- const end = Date.now();
70
+ catch (recError) {
71
+ const recEnd = Date.now();
72
+ const errorDetail = recError instanceof Error ? recError.message : String(recError);
140
73
  this.traces.push({
141
- name,
142
- start,
143
- end,
144
- duration: end - start,
145
- error: errorStr,
146
- metadata: errorMeta,
74
+ name: `${name}:recovery`,
75
+ start: recStart,
76
+ end: recEnd,
77
+ duration: recEnd - recStart,
78
+ error: errorDetail,
79
+ metadata: { success: false, phase: 'RECOVERY_FAILURE' },
147
80
  });
81
+ getLogger().audit('PIPELINE_RECOVERY_FAILED', { step: name, originalError: errorStr, recoveryError: errorDetail }, { source: 'system', severity: 'high', scope: 'session' });
148
82
  }
83
+ return null; // Always re-throw original error
149
84
  });
150
85
  return new Pipeline(nextPromise, this.startTime, name, this.traces, this.ctxRef);
151
86
  }
152
87
  /**
153
- * Add a step with error recovery
88
+ * Core step execution shared by step() and stepWithRecovery().
89
+ * Handles: abort check, phase events, plan journaling, tracing.
90
+ * The onError callback runs recovery/post-processing; return non-null to inject error metadata.
154
91
  */
155
- stepWithRecovery(name, action, recovery) {
156
- const nextPromise = this.promise.then(async (ctx) => {
157
- const start = Date.now();
158
- let phaseStarted = false;
159
- let abortedBeforeAction = false;
160
- let errorStr;
161
- let errorMeta;
162
- let result;
163
- const emit = ctx.emit;
164
- const isPhase = (value) => EXECUTION_PHASES.includes(value);
165
- const planRuntime = ctx?.planRuntime;
166
- const persistenceRoot = ctx?.workspace?.baseRepoPath || ctx?.workspace?.workPath;
167
- const attempt = ctx?.attempt ?? 1;
168
- const tryAppendPlanNote = async (note) => {
169
- if (!planRuntime || !persistenceRoot)
170
- return;
171
- try {
172
- await appendPlanNote({
173
- persistenceRoot,
174
- sessionId: planRuntime.sessionId,
175
- note,
176
- });
177
- recordAuditEvent('plan.runtime.note.append', { note, ok: true }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
178
- return true;
179
- }
180
- catch (e) {
181
- const msg = e instanceof Error ? e.message : String(e);
182
- recordAuditEvent('plan.runtime.note.append.failed', { note, error: msg }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
183
- getLogger().debug(`[PlanRuntime] Failed to append note: ${msg}`);
184
- return false;
185
- }
186
- };
92
+ async executeStep(name, action, onError) {
93
+ const start = Date.now();
94
+ let phaseStarted = false;
95
+ let errorStr;
96
+ let errorMeta;
97
+ let result;
98
+ const ctx = await this.promise;
99
+ const emit = ctx.emit;
100
+ const isPhase = (value) => EXECUTION_PHASES.includes(value);
101
+ const ctxObj = isRecord(ctx) ? ctx : null;
102
+ const planRuntime = ctxObj?.planRuntime;
103
+ const workspace = isRecord(ctxObj?.workspace) ? ctxObj.workspace : null;
104
+ const persistenceRoot = (typeof workspace?.baseRepoPath === 'string' ? workspace.baseRepoPath : undefined) ||
105
+ (typeof workspace?.workPath === 'string' ? workspace.workPath : undefined);
106
+ const attempt = typeof ctxObj?.attempt === 'number' ? ctxObj.attempt : 1;
107
+ const tryAppendPlanNote = async (note) => {
108
+ if (!planRuntime || !persistenceRoot)
109
+ return;
187
110
  try {
188
- this.ctxRef.current = ctx;
189
- const signal = ctx?.options?.signal;
190
- const strategy = ctx?.workspace?.strategy ?? ctx?.options?.strategy;
191
- if (signal?.aborted && strategy === 'worktree') {
192
- abortedBeforeAction = true;
193
- throw new Error('Operation cancelled by user');
194
- }
195
- setAuditContext({ phase: name });
196
- if (emit && isPhase(name)) {
197
- emit({ type: 'phase.start', phase: name, timestamp: new Date() });
198
- phaseStarted = true;
199
- const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.start ${name}`);
200
- if (planRuntime && ok !== undefined) {
201
- emit({
202
- type: 'plan.runtime.journal',
203
- sessionId: planRuntime.sessionId,
204
- phase: name,
205
- kind: 'start',
206
- attempt,
207
- ok,
208
- timestamp: new Date(),
209
- });
210
- }
211
- }
212
- result = await action(ctx);
213
- this.ctxRef.current = result;
214
- return result;
111
+ await appendPlanNote({ persistenceRoot, sessionId: planRuntime.sessionId, note });
112
+ recordAuditEvent('plan.runtime.note.append', { note, ok: true }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
113
+ return true;
215
114
  }
216
- catch (error) {
217
- errorStr = error instanceof Error ? error.message : String(error);
218
- errorMeta =
219
- typeof error === 'object' && error !== null
220
- ? {
221
- name: error.name,
222
- code: error.code,
223
- llmCode: error.llmCode,
224
- }
225
- : undefined;
226
- if (abortedBeforeAction) {
227
- throw error;
228
- }
229
- // Trigger Recovery
230
- const recStart = Date.now();
231
- try {
232
- await recovery(ctx);
233
- this.traces.push({
234
- name: `${name}:recovery`,
235
- start: recStart,
236
- end: Date.now(),
237
- duration: Date.now() - recStart,
238
- metadata: { success: true },
115
+ catch (e) {
116
+ const msg = e instanceof Error ? e.message : String(e);
117
+ recordAuditEvent('plan.runtime.note.append.failed', { note, error: msg }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
118
+ getLogger().debug(`[PlanRuntime] Failed to append note: ${msg}`);
119
+ return false;
120
+ }
121
+ };
122
+ try {
123
+ this.ctxRef.current = ctx;
124
+ const options = isRecord(ctxObj?.options) ? ctxObj.options : null;
125
+ const signal = options?.signal;
126
+ const strategy = (workspace?.strategy ?? options?.strategy);
127
+ if (signal?.aborted && strategy === 'worktree') {
128
+ throw new Error('Operation cancelled by user');
129
+ }
130
+ setAuditContext({ phase: name });
131
+ if (emit && isPhase(name)) {
132
+ emit({ type: 'phase.start', phase: name, timestamp: new Date() });
133
+ phaseStarted = true;
134
+ const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.start ${name}`);
135
+ if (planRuntime && ok !== undefined) {
136
+ emit({
137
+ type: 'plan.runtime.journal',
138
+ sessionId: planRuntime.sessionId,
139
+ phase: name,
140
+ kind: 'start',
141
+ attempt,
142
+ ok,
143
+ timestamp: new Date(),
239
144
  });
240
145
  }
241
- catch (recError) {
242
- const recEnd = Date.now();
243
- const errorDetail = recError instanceof Error ? recError.message : String(recError);
244
- // 1. Record recovery failure to internal traces
245
- this.traces.push({
246
- name: `${name}:recovery`,
247
- start: recStart,
248
- end: recEnd,
249
- duration: recEnd - recStart,
250
- error: errorDetail,
251
- metadata: { success: false, phase: 'RECOVERY_FAILURE' },
252
- });
253
- // 2. Force audit log to disk (persistent storage)
254
- getLogger().audit('PIPELINE_RECOVERY_FAILED', {
255
- step: name,
256
- originalError: errorStr,
257
- recoveryError: errorDetail,
258
- }, { source: 'system', severity: 'high', scope: 'session' });
146
+ }
147
+ result = await action(ctx);
148
+ this.ctxRef.current = result;
149
+ // Check for structured failures (e.g., APPLY_BACK)
150
+ const postResult = await onError(ctx, '');
151
+ if (postResult) {
152
+ errorStr = postResult.errorStr;
153
+ errorMeta = postResult.errorMeta;
154
+ }
155
+ return result;
156
+ }
157
+ catch (error) {
158
+ errorStr = error instanceof Error ? error.message : String(error);
159
+ errorMeta =
160
+ typeof error === 'object' && error !== null
161
+ ? {
162
+ name: error.name,
163
+ code: error.code,
164
+ llmCode: error.llmCode,
165
+ }
166
+ : undefined;
167
+ // Skip recovery on abort - the operation was cancelled, not failed
168
+ if (errorStr !== 'Operation cancelled by user') {
169
+ // Run recovery/post-processing
170
+ const postResult = await onError(ctx, errorStr);
171
+ if (postResult?.errorStr) {
172
+ errorStr = postResult.errorStr;
173
+ errorMeta = postResult.errorMeta;
259
174
  }
260
- throw error; // Propagate original error
261
175
  }
262
- finally {
263
- if (emit && isPhase(name) && phaseStarted) {
176
+ throw error;
177
+ }
178
+ finally {
179
+ if (emit && isPhase(name) && phaseStarted) {
180
+ emit({ type: 'phase.end', phase: name, success: !errorStr, timestamp: new Date() });
181
+ const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.end ${name} (success=${String(!errorStr)})`);
182
+ if (planRuntime && ok !== undefined) {
264
183
  emit({
265
- type: 'phase.end',
184
+ type: 'plan.runtime.journal',
185
+ sessionId: planRuntime.sessionId,
266
186
  phase: name,
267
- success: !errorStr,
187
+ kind: 'end',
188
+ attempt,
189
+ ok,
268
190
  timestamp: new Date(),
269
191
  });
270
- const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.end ${name} (success=${String(!errorStr)})`);
271
- if (planRuntime && ok !== undefined) {
272
- emit({
273
- type: 'plan.runtime.journal',
274
- sessionId: planRuntime.sessionId,
275
- phase: name,
276
- kind: 'end',
277
- attempt,
278
- ok,
279
- timestamp: new Date(),
280
- });
281
- }
282
192
  }
283
- setAuditContext({ phase: undefined });
284
- const end = Date.now();
285
- this.traces.push({
286
- name,
287
- start,
288
- end,
289
- duration: end - start,
290
- error: errorStr,
291
- metadata: errorMeta,
292
- });
293
193
  }
294
- });
295
- return new Pipeline(nextPromise, this.startTime, name, this.traces, this.ctxRef);
194
+ setAuditContext({ phase: undefined });
195
+ const end = Date.now();
196
+ this.traces.push({
197
+ name,
198
+ start,
199
+ end,
200
+ duration: end - start,
201
+ error: errorStr,
202
+ metadata: errorMeta,
203
+ });
204
+ }
296
205
  }
297
206
  /**
298
207
  * Execute the pipeline and get the final result
@@ -5,7 +5,10 @@ import { mapErrorForDisplay } from '../../../observability/error-mapping.js';
5
5
  import { resolveExecutionProfile } from '../../../runtime/execution-profile.js';
6
6
  import { isRecoverableToolInputErrorCode } from '../../../tools/recoverable-tool-errors.js';
7
7
  import { EXECUTION_PHASES } from '../../../types/runtime.js';
8
+ import { isRecord } from '../../../utils/serialize.js';
8
9
  import { classifyError, isRetryable } from '../../../verification/runner.js';
10
+ // Used by non-autopilot modes to determine retry eligibility.
11
+ // Autopilot overrides via finalize() — this set has no effect there.
9
12
  const RETRYABLE_PHASES = new Set([
10
13
  'CONTEXT',
11
14
  'EXPLORE',
@@ -29,7 +32,7 @@ function inferFailurePhase(flowReport) {
29
32
  }
30
33
  return 'VERIFY';
31
34
  }
32
- function extractErrorCode(error) {
35
+ export function extractErrorCode(error) {
33
36
  if (typeof error === 'object' && error !== null) {
34
37
  const err = error;
35
38
  // Prioritize code (from SalmonError) for better specificity
@@ -49,28 +52,52 @@ function sanitizeReason(value) {
49
52
  return mapErrorForDisplay({ message: sanitized }).message;
50
53
  }
51
54
  function extractInputRequired(error) {
52
- if (!error || typeof error !== 'object')
55
+ if (!isRecord(error))
53
56
  return undefined;
54
57
  const value = 'inputRequired' in error ? error.inputRequired : error;
55
- if (!value || typeof value !== 'object')
58
+ if (!isRecord(value))
56
59
  return undefined;
57
60
  if (typeof value.prompt !== 'string' || typeof value.type !== 'string')
58
61
  return undefined;
59
- return value;
62
+ const result = { type: value.type, prompt: value.prompt };
63
+ if (value.reason === 'approval' ||
64
+ value.reason === 'clarification' ||
65
+ value.reason === 'reopen') {
66
+ result.reason = value.reason;
67
+ }
68
+ if (Array.isArray(value.questions)) {
69
+ result.questions = value.questions;
70
+ }
71
+ if (typeof value.responseFormat === 'string') {
72
+ result.responseFormat = value.responseFormat;
73
+ }
74
+ return result;
60
75
  }
61
76
  function extractInterrupt(error) {
62
- if (!error || typeof error !== 'object')
77
+ if (!isRecord(error))
63
78
  return undefined;
64
79
  const value = error.interrupt;
65
- if (!value || typeof value !== 'object')
80
+ if (!isRecord(value))
66
81
  return undefined;
67
82
  if (typeof value.type !== 'string')
68
83
  return undefined;
69
- return value;
84
+ const result = {
85
+ type: value.type,
86
+ };
87
+ if (typeof value.reason === 'string')
88
+ result.reason = value.reason;
89
+ if (typeof value.prompt === 'string')
90
+ result.prompt = value.prompt;
91
+ if (isRecord(value.data))
92
+ result.data = value.data;
93
+ return result;
70
94
  }
71
95
  export function resolveAttemptFailure(params) {
72
96
  const { flowReport, context, flowMode } = params;
73
97
  const profile = resolveExecutionProfile(flowMode);
98
+ // Autopilot is one-shot: never retry at the transaction-runner level.
99
+ const finalize = (failure) => flowMode === 'autopilot' ? { ...failure, retryable: false } : failure;
100
+ const effectiveVerifyPolicy = params.verifyPolicy ?? profile.verifyPolicy;
74
101
  const interrupt = extractInterrupt(flowReport.error);
75
102
  const interruptCode = extractErrorCode(flowReport.error);
76
103
  if (interruptCode === 'INTERRUPT_REQUIRED' && interrupt?.type === 'awaiting_input') {
@@ -92,7 +119,7 @@ export function resolveAttemptFailure(params) {
92
119
  };
93
120
  }
94
121
  const autopilotCompletion = flowMode === 'autopilot' && context && 'completion' in context ? context.completion : undefined;
95
- const verifyOk = profile.verifyPolicy === 'never' ? true : context?.verifyResult?.ok !== false;
122
+ const verifyOk = effectiveVerifyPolicy === 'never' ? true : context?.verifyResult?.ok !== false;
96
123
  const applyBackResult = context && 'applyBackResult' in context ? context.applyBackResult : undefined;
97
124
  const applyBackFailed = profile.failurePolicy === 'rollback' &&
98
125
  applyBackResult?.success === false &&
@@ -122,7 +149,7 @@ export function resolveAttemptFailure(params) {
122
149
  return undefined;
123
150
  }
124
151
  const errorCode = extractErrorCode(flowReport.error) ?? extractErrorCodeFromTraces(flowReport);
125
- if (profile.verifyPolicy !== 'never' && context?.verifyResult?.ok === false) {
152
+ if (effectiveVerifyPolicy !== 'never' && context?.verifyResult?.ok === false) {
126
153
  const verifyOutput = context.verifyResult.output || text.loop.loopExecutionFailed;
127
154
  const errorType = classifyError(verifyOutput);
128
155
  const fallbackReason = sanitizeReason(context.lastError || verifyOutput);
@@ -134,7 +161,7 @@ export function resolveAttemptFailure(params) {
134
161
  environmentMode,
135
162
  fallbackReason,
136
163
  });
137
- return {
164
+ return finalize({
138
165
  reason: guidance.safeHint,
139
166
  reasonCode: 'VERIFY_FAILED',
140
167
  failurePhase: 'VERIFY',
@@ -143,7 +170,7 @@ export function resolveAttemptFailure(params) {
143
170
  diagnosticCode: guidance.diagnosticCode,
144
171
  safeHint: guidance.safeHint,
145
172
  remediationSteps: guidance.remediationSteps,
146
- };
173
+ });
147
174
  }
148
175
  if (flowMode === 'autopilot' && autopilotCompletion) {
149
176
  if (autopilotCompletion.status === 'changed' ||
@@ -151,13 +178,11 @@ export function resolveAttemptFailure(params) {
151
178
  if (flowReport.success && verifyOk)
152
179
  return undefined;
153
180
  }
154
- const reasonCode = autopilotCompletion.status === 'verification_missing'
155
- ? 'VERIFY_COMMAND_MISSING'
156
- : autopilotCompletion.status === 'tool_failure' &&
157
- isRecoverableToolInputErrorCode(autopilotCompletion.errorCode)
158
- ? 'TOOL_CORRECTION_REQUIRED'
159
- : 'LOOP_FAILED';
160
- const failurePhase = autopilotCompletion.status === 'verification_missing' ? 'VERIFY' : 'AUTOPILOT';
181
+ const reasonCode = autopilotCompletion.status === 'tool_failure' &&
182
+ isRecoverableToolInputErrorCode(autopilotCompletion.errorCode)
183
+ ? 'TOOL_CORRECTION_REQUIRED'
184
+ : 'LOOP_FAILED';
185
+ const failurePhase = 'AUTOPILOT';
161
186
  const fallbackReason = autopilotCompletion.reason ||
162
187
  (autopilotCompletion.status === 'no_effect'
163
188
  ? 'Autopilot completed without changing files or producing an answer.'
@@ -169,7 +194,7 @@ export function resolveAttemptFailure(params) {
169
194
  environmentMode,
170
195
  fallbackReason,
171
196
  });
172
- return {
197
+ return finalize({
173
198
  reason: guidance.safeHint,
174
199
  reasonCode,
175
200
  failurePhase,
@@ -178,7 +203,7 @@ export function resolveAttemptFailure(params) {
178
203
  diagnosticCode: guidance.diagnosticCode,
179
204
  safeHint: guidance.safeHint,
180
205
  remediationSteps: guidance.remediationSteps,
181
- };
206
+ });
182
207
  }
183
208
  if (errorCode === 'PREFLIGHT_NOT_GIT') {
184
209
  const fallbackReason = sanitizeReason(flowReport.error);
@@ -271,7 +296,7 @@ export function resolveAttemptFailure(params) {
271
296
  environmentMode,
272
297
  fallbackReason,
273
298
  });
274
- return {
299
+ return finalize({
275
300
  reason: guidance.safeHint,
276
301
  reasonCode: 'TOOL_CORRECTION_REQUIRED',
277
302
  failurePhase,
@@ -280,7 +305,7 @@ export function resolveAttemptFailure(params) {
280
305
  diagnosticCode: guidance.diagnosticCode,
281
306
  safeHint: guidance.safeHint,
282
307
  remediationSteps: guidance.remediationSteps,
283
- };
308
+ });
284
309
  }
285
310
  const guidance = buildFailureGuidance({
286
311
  reasonCode: failurePhase === 'ROLLBACK' ? 'ROLLBACK_FAILED' : 'LOOP_FAILED',
@@ -303,7 +328,7 @@ export function resolveAttemptFailure(params) {
303
328
  remediationSteps: guidance.remediationSteps,
304
329
  };
305
330
  }
306
- return {
331
+ return finalize({
307
332
  reason: guidance.safeHint,
308
333
  reasonCode: 'LOOP_FAILED',
309
334
  failurePhase,
@@ -312,6 +337,6 @@ export function resolveAttemptFailure(params) {
312
337
  diagnosticCode: guidance.diagnosticCode,
313
338
  safeHint: guidance.safeHint,
314
339
  remediationSteps: guidance.remediationSteps,
315
- };
340
+ });
316
341
  }
317
342
  //# sourceMappingURL=attempt-failure.js.map
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '../../../utils/serialize.js';
1
2
  export function buildAuthorizationSummary(logs) {
2
3
  if (!logs || logs.length === 0)
3
4
  return null;
@@ -11,7 +12,7 @@ export function buildAuthorizationSummary(logs) {
11
12
  };
12
13
  let hasEntries = false;
13
14
  for (const entry of logs) {
14
- if (!entry || entry.eventType !== 'authorization')
15
+ if (!isRecord(entry) || entry.eventType !== 'authorization')
15
16
  continue;
16
17
  const source = entry.authSource;
17
18
  if (source === 'auto') {