@sayknow-cli/coding-agent 0.2.3 → 0.2.5

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 (159) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/types/cli/migrate-cli.d.ts +20 -0
  3. package/dist/types/cli/update-cli.d.ts +3 -0
  4. package/dist/types/commands/migrate.d.ts +33 -0
  5. package/dist/types/config/keybindings.d.ts +4 -0
  6. package/dist/types/config/model-registry.d.ts +3 -0
  7. package/dist/types/config/models-config-schema.d.ts +5 -0
  8. package/dist/types/config/settings-schema.d.ts +27 -0
  9. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  10. package/dist/types/hooks/skill-state.d.ts +12 -4
  11. package/dist/types/i18n/messages/en.d.ts +14 -0
  12. package/dist/types/lsp/startup-events.d.ts +1 -0
  13. package/dist/types/migrate/action-planner.d.ts +11 -0
  14. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  15. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  16. package/dist/types/migrate/adapters/index.d.ts +45 -0
  17. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  18. package/dist/types/migrate/executor.d.ts +2 -0
  19. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  20. package/dist/types/migrate/report.d.ts +18 -0
  21. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  22. package/dist/types/migrate/types.d.ts +126 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  24. package/dist/types/modes/components/welcome.d.ts +3 -1
  25. package/dist/types/modes/interactive-mode.d.ts +3 -0
  26. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  27. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  28. package/dist/types/research-plan/index.d.ts +1 -0
  29. package/dist/types/research-plan/ledger.d.ts +33 -0
  30. package/dist/types/rlm/artifacts.d.ts +1 -1
  31. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  32. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +2 -0
  33. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +2 -2
  34. package/dist/types/skc-runtime/goal-mode-request.d.ts +1 -1
  35. package/dist/types/skc-runtime/session-layout.d.ts +59 -0
  36. package/dist/types/skc-runtime/session-resolution.d.ts +47 -0
  37. package/dist/types/skc-runtime/state-graph.d.ts +1 -1
  38. package/dist/types/skc-runtime/state-runtime.d.ts +5 -4
  39. package/dist/types/skc-runtime/state-schema.d.ts +2 -0
  40. package/dist/types/skc-runtime/state-writer.d.ts +36 -7
  41. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  42. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +7 -4
  43. package/dist/types/skc-runtime/workflow-command-ref.d.ts +1 -1
  44. package/dist/types/skc-runtime/workflow-manifest.d.ts +1 -1
  45. package/dist/types/skill-state/active-state.d.ts +6 -11
  46. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  47. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +5 -0
  48. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  49. package/dist/types/task/spawn-gate.d.ts +1 -10
  50. package/package.json +7 -7
  51. package/scripts/build-binary.ts +0 -7
  52. package/src/cli/migrate-cli.ts +106 -0
  53. package/src/cli/setup-cli.ts +14 -1
  54. package/src/cli/update-cli.ts +53 -3
  55. package/src/cli.ts +1 -0
  56. package/src/commands/deep-interview.ts +2 -2
  57. package/src/commands/launch.ts +1 -1
  58. package/src/commands/migrate.ts +46 -0
  59. package/src/commands/state.ts +2 -1
  60. package/src/commands/team.ts +7 -3
  61. package/src/config/model-registry.ts +9 -2
  62. package/src/config/model-resolver.ts +13 -2
  63. package/src/config/models-config-schema.ts +1 -0
  64. package/src/config/settings-schema.ts +17 -0
  65. package/src/coordinator-mcp/policy.ts +10 -2
  66. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +0 -1
  67. package/src/defaults/skc/skills/deep-interview/SKILL.md +30 -24
  68. package/src/defaults/skc/skills/ralplan/SKILL.md +10 -4
  69. package/src/defaults/skc/skills/team/SKILL.md +51 -47
  70. package/src/defaults/skc/skills/ultragoal/SKILL.md +17 -13
  71. package/src/exec/bash-executor.ts +3 -1
  72. package/src/extensibility/custom-commands/loader.ts +0 -7
  73. package/src/extensibility/skc-plugins/injection.ts +23 -4
  74. package/src/extensibility/skc-plugins/state.ts +16 -1
  75. package/src/harness-control-plane/storage.ts +14 -4
  76. package/src/hooks/native-skill-hook.ts +38 -12
  77. package/src/hooks/skill-state.ts +178 -83
  78. package/src/i18n/messages/de.ts +14 -0
  79. package/src/i18n/messages/en.ts +14 -0
  80. package/src/i18n/messages/es.ts +14 -0
  81. package/src/i18n/messages/fr.ts +14 -0
  82. package/src/i18n/messages/ja.ts +14 -0
  83. package/src/i18n/messages/ko.ts +14 -0
  84. package/src/i18n/messages/zh.ts +14 -0
  85. package/src/internal-urls/docs-index.generated.ts +11 -9
  86. package/src/lsp/startup-events.ts +24 -0
  87. package/src/migrate/action-planner.ts +318 -0
  88. package/src/migrate/adapters/claude-code.ts +39 -0
  89. package/src/migrate/adapters/codex.ts +70 -0
  90. package/src/migrate/adapters/index.ts +277 -0
  91. package/src/migrate/adapters/opencode.ts +52 -0
  92. package/src/migrate/executor.ts +81 -0
  93. package/src/migrate/mcp-mapper.ts +152 -0
  94. package/src/migrate/report.ts +104 -0
  95. package/src/migrate/skill-normalizer.ts +80 -0
  96. package/src/migrate/types.ts +163 -0
  97. package/src/modes/bridge/bridge-mode.ts +2 -2
  98. package/src/modes/components/custom-editor.ts +30 -20
  99. package/src/modes/components/model-selector.ts +42 -18
  100. package/src/modes/components/welcome.ts +20 -9
  101. package/src/modes/controllers/input-controller.ts +21 -3
  102. package/src/modes/interactive-mode.ts +27 -19
  103. package/src/modes/prompt-action-autocomplete.ts +11 -1
  104. package/src/modes/rpc/rpc-mode.ts +2 -2
  105. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  106. package/src/prompts/agents/init.md +1 -1
  107. package/src/prompts/system/plan-mode-active.md +1 -1
  108. package/src/prompts/tools/ast-grep.md +1 -1
  109. package/src/prompts/tools/search.md +1 -1
  110. package/src/prompts/tools/task.md +1 -2
  111. package/src/research-plan/index.ts +1 -0
  112. package/src/research-plan/ledger.ts +177 -0
  113. package/src/rlm/artifacts.ts +12 -3
  114. package/src/rlm/index.ts +7 -0
  115. package/src/runtime-mcp/config-writer.ts +46 -0
  116. package/src/session/agent-session.ts +43 -41
  117. package/src/session/session-manager.ts +19 -2
  118. package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
  119. package/src/setup/hermes-setup.ts +1 -1
  120. package/src/skc-runtime/deep-interview-recorder.ts +51 -18
  121. package/src/skc-runtime/deep-interview-runtime.ts +49 -23
  122. package/src/skc-runtime/goal-mode-request.ts +26 -11
  123. package/src/skc-runtime/launch-tmux.ts +68 -15
  124. package/src/skc-runtime/ralplan-runtime.ts +79 -50
  125. package/src/skc-runtime/session-layout.ts +180 -0
  126. package/src/skc-runtime/session-resolution.ts +217 -0
  127. package/src/skc-runtime/state-graph.ts +1 -2
  128. package/src/skc-runtime/state-migrations.ts +1 -0
  129. package/src/skc-runtime/state-runtime.ts +237 -114
  130. package/src/skc-runtime/state-schema.ts +2 -0
  131. package/src/skc-runtime/state-writer.ts +310 -42
  132. package/src/skc-runtime/team-runtime.ts +43 -19
  133. package/src/skc-runtime/tmux-sessions.ts +43 -2
  134. package/src/skc-runtime/ultragoal-guard.ts +45 -2
  135. package/src/skc-runtime/ultragoal-runtime.ts +121 -41
  136. package/src/skc-runtime/workflow-command-ref.ts +1 -2
  137. package/src/skc-runtime/workflow-manifest.ts +1 -2
  138. package/src/skill-state/active-state.ts +116 -129
  139. package/src/skill-state/canonical-skills.ts +4 -0
  140. package/src/skill-state/deep-interview-mutation-guard.ts +238 -111
  141. package/src/skill-state/workflow-hud.ts +4 -2
  142. package/src/skill-state/workflow-state-contract.ts +3 -3
  143. package/src/slash-commands/builtin-registry.ts +8 -4
  144. package/src/system-prompt.ts +11 -9
  145. package/src/task/agents.ts +1 -22
  146. package/src/task/index.ts +1 -41
  147. package/src/task/spawn-gate.ts +1 -38
  148. package/src/task/types.ts +1 -1
  149. package/src/tools/ask.ts +34 -12
  150. package/src/tools/ast-edit.ts +2 -2
  151. package/src/tools/computer.ts +58 -4
  152. package/src/utils/edit-mode.ts +1 -1
  153. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  154. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  155. package/src/prompts/agents/explore.md +0 -58
  156. package/src/prompts/agents/plan.md +0 -49
  157. package/src/prompts/agents/reviewer.md +0 -141
  158. package/src/prompts/agents/task.md +0 -16
  159. package/src/prompts/review-request.md +0 -70
@@ -44,7 +44,7 @@ import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slas
44
44
  import { type Goal, type GoalModeState, normalizeGoal } from "../goals/state";
45
45
  import { t } from "../i18n";
46
46
  import { resolveLocalUrlToPath } from "../internal-urls";
47
- import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
47
+ import { getLspStartupWarningMessage, LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
48
48
  import {
49
49
  humanizePlanTitle,
50
50
  type PlanApprovalDetails,
@@ -72,6 +72,7 @@ import { normalizeLocalScheme } from "../tools/path-utils";
72
72
  import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
73
73
  import { formatPhaseDisplayName } from "../tools/todo-write";
74
74
  import { ToolError } from "../tools/tool-errors";
75
+
75
76
  import type { EventBus } from "../utils/event-bus";
76
77
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
77
78
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
@@ -86,7 +87,11 @@ import type { HookInputComponent } from "./components/hook-input";
86
87
  import type { HookSelectorComponent } from "./components/hook-selector";
87
88
  import { StatusLineComponent } from "./components/status-line";
88
89
  import type { ToolExecutionHandle } from "./components/tool-execution";
89
- import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
90
+ import {
91
+ WelcomeComponent,
92
+ type WelcomeLogoMode,
93
+ type LspServerInfo as WelcomeLspServerInfo,
94
+ } from "./components/welcome";
90
95
  import { BtwController } from "./controllers/btw-controller";
91
96
  import { CommandController } from "./controllers/command-controller";
92
97
  import { EventController } from "./controllers/event-controller";
@@ -218,6 +223,21 @@ function parseGoalSubcommand(args: string): { sub: GoalSubcommand | undefined; r
218
223
  return { sub: undefined, rest: trimmed };
219
224
  }
220
225
 
226
+ export type WelcomeBannerSettingMode = "auto" | "unicode" | "square" | "ascii";
227
+
228
+ export function resolveWelcomeLogoMode(
229
+ mode: WelcomeBannerSettingMode,
230
+ env: Record<string, string | undefined> = Bun.env,
231
+ platform: NodeJS.Platform = process.platform,
232
+ ): WelcomeLogoMode {
233
+ void env;
234
+ void platform;
235
+ if (mode === "unicode") return "unicode";
236
+ if (mode === "square") return "square";
237
+ if (mode === "ascii") return "ascii";
238
+ return "unicode";
239
+ }
240
+
221
241
  /** Options for creating an InteractiveMode instance (for future API use) */
222
242
  export interface InteractiveModeOptions {
223
243
  /** Providers that were migrated during startup */
@@ -479,6 +499,7 @@ export class InteractiveMode implements InteractiveModeContext {
479
499
  );
480
500
 
481
501
  const startupQuiet = settings.get("startup.quiet");
502
+ const welcomeLogoMode = resolveWelcomeLogoMode(settings.get("startup.welcomeBannerMode"));
482
503
  this.#welcomeComponent = undefined;
483
504
 
484
505
  for (const warning of this.session.configWarnings) {
@@ -494,6 +515,7 @@ export class InteractiveMode implements InteractiveModeContext {
494
515
  providerName,
495
516
  recentSessions,
496
517
  this.#getWelcomeLspServers(),
518
+ welcomeLogoMode,
497
519
  );
498
520
 
499
521
  // Setup UI layout
@@ -2062,23 +2084,9 @@ export class InteractiveMode implements InteractiveModeContext {
2062
2084
  #handleLspStartupEvent(event: LspStartupEvent): void {
2063
2085
  this.#updateWelcomeLspServers();
2064
2086
 
2065
- if (event.type === "failed") {
2066
- this.showWarning(`LSP startup failed: ${event.error}. It will retry lazily on write.`);
2067
- return;
2068
- }
2069
-
2070
- const failedServers = event.servers.filter(server => server.status === "error");
2071
-
2072
- if (failedServers.length === 1) {
2073
- const failedServer = failedServers[0];
2074
- const detail = failedServer.error ? `: ${failedServer.error}` : "";
2075
- this.showWarning(`LSP startup failed for ${failedServer.name}${detail}. It will retry lazily on write.`);
2076
- return;
2077
- }
2078
-
2079
- if (failedServers.length > 1) {
2080
- const failedNames = failedServers.map(server => server.name).join(", ");
2081
- this.showWarning(`LSP startup failed for ${failedNames}. It will retry lazily on write.`);
2087
+ const warningMessage = getLspStartupWarningMessage(event);
2088
+ if (warningMessage) {
2089
+ this.showWarning(warningMessage);
2082
2090
  }
2083
2091
  }
2084
2092
 
@@ -28,6 +28,7 @@ interface PromptActionAutocompleteOptions {
28
28
  keybindings: KeybindingsManager;
29
29
  copyCurrentLine: () => void;
30
30
  copyPrompt: () => void;
31
+ pasteImage: () => void;
31
32
  undo: (prefix: string) => void;
32
33
  moveCursorToMessageEnd: () => void;
33
34
  moveCursorToMessageStart: () => void;
@@ -190,7 +191,9 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
190
191
  const query = promptActionPrefix.slice(1).toLowerCase();
191
192
  const items = this.#actions
192
193
  .map(action => {
193
- const searchable = [action.label, action.description, ...action.keywords].join(" ").toLowerCase();
194
+ const searchable = [action.id, action.label, action.description, ...action.keywords]
195
+ .join(" ")
196
+ .toLowerCase();
194
197
  if (!fuzzyMatch(query, searchable)) return null;
195
198
  return {
196
199
  value: action.label,
@@ -368,6 +371,13 @@ export function createPromptActionAutocompleteProvider(
368
371
  keywords: ["copy", "prompt", "clipboard", "message"],
369
372
  execute: options.copyPrompt,
370
373
  },
374
+ {
375
+ id: "paste-image",
376
+ label: "Paste image from clipboard",
377
+ description: formatKeyHints(options.keybindings.getKeys("app.clipboard.pasteImage")),
378
+ keywords: ["paste", "image", "clipboard", "screenshot", "attach", "vision"],
379
+ execute: options.pasteImage,
380
+ },
371
381
  {
372
382
  id: "undo",
373
383
  label: "Undo",
@@ -11,7 +11,6 @@
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
 
14
- import * as path from "node:path";
15
14
  import { $pickenv, logger, readLines, Snowflake } from "@sayknow-cli/utils";
16
15
  import type {
17
16
  ExtensionUIContext,
@@ -20,6 +19,7 @@ import type {
20
19
  } from "../../extensibility/extensions";
21
20
  import { type Theme, theme } from "../../modes/theme/theme";
22
21
  import type { AgentSession } from "../../session/agent-session";
22
+ import { workflowGatePath } from "../../skc-runtime/session-layout";
23
23
  import { initializeExtensions } from "../runtime-init";
24
24
  import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
25
25
  import { AgentWireFrameSequencer, toAgentWireEventFrame } from "../shared/agent-wire/event-envelope";
@@ -336,7 +336,7 @@ export async function runRpcMode(
336
336
  // Unattended control plane (#318/#319/#323/G011): routes negotiate_unattended +
337
337
  // workflow_gate_response and lets skill runtimes emit gates over RPC.
338
338
  const gateStore = new FileGateStore(
339
- path.join(session.sessionManager.getCwd(), ".skc", "state", "workflow-gates", `${session.sessionId}.json`),
339
+ workflowGatePath(session.sessionManager.getCwd(), session.sessionId, session.sessionId),
340
340
  );
341
341
  const unattendedControlPlane = new UnattendedSessionControlPlane({
342
342
  runId: session.sessionId,
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
14
14
  import * as path from "node:path";
15
+ import { sessionAuditDir } from "../../../skc-runtime/session-layout";
15
16
  import type { RpcBudgetExceeded, RpcWorkflowGateKind, RpcWorkflowStage } from "../../rpc/rpc-types";
16
17
  import { answerHashOf } from "./workflow-gate-schema";
17
18
 
@@ -69,9 +70,9 @@ function defaultId(): string {
69
70
  return `ae_${Date.now().toString(36)}_${idCounter.toString(36)}`;
70
71
  }
71
72
 
72
- export function defaultAuditPath(runId: string, root = process.cwd()): string {
73
+ export function defaultAuditPath(runId: string, root = process.cwd(), skcSessionId = runId): string {
73
74
  const safe = runId.replace(/[^a-zA-Z0-9_.-]/g, "_");
74
- return path.join(root, ".skc", "audit", "unattended", `${safe}.jsonl`);
75
+ return path.join(sessionAuditDir(root, skcSessionId), "unattended", `${safe}.jsonl`);
75
76
  }
76
77
 
77
78
  /** Append-only audit log writer + reader for one unattended run. */
@@ -5,7 +5,7 @@ thinking-level: medium
5
5
  hide: true
6
6
  ---
7
7
 
8
- Generate AGENTS.md by launching multiple `explore` agents in parallel (via `task` tool) scanning different areas (core src, tests, configs/build, scripts/docs), then synthesize findings into a single file.
8
+ Generate AGENTS.md by launching multiple canonical role agents in parallel (via `task` tool, usually `planner` or `architect`) scanning different areas (core src, tests, configs/build, scripts/docs), then synthesize findings into a single file.
9
9
 
10
10
  <structure>
11
11
  - **Project Overview**: Brief description of project purpose
@@ -82,7 +82,7 @@ The plan MUST be scannable yet detailed enough to execute.
82
82
 
83
83
  <procedure>
84
84
  ### Phase 1: Understand
85
- You MUST focus on the request and associated code. You SHOULD launch parallel explore agents when scope spans multiple areas.
85
+ You MUST focus on the request and associated code. You SHOULD launch parallel canonical role agents (`planner` or `architect`) when scope spans multiple areas.
86
86
 
87
87
  ### Phase 2: Design
88
88
  You MUST draft an approach based on exploration. You MUST consider trade-offs briefly, then choose.
@@ -38,5 +38,5 @@ Performs structural code search using AST matching via native ast-grep.
38
38
  <critical>
39
39
  - Avoid repo-root scans — narrow `paths` first
40
40
  - Parse issues are query failure, not evidence of absence: repair the pattern or tighten `paths` before concluding "no matches"
41
- - For broad/open-ended exploration across subsystems, use Task tool with explore subagent first
41
+ - For broad/open-ended inspection across subsystems, delegate a bounded fact-finding task to an appropriate canonical role agent (`planner` or `architect`) first
42
42
  </critical>
@@ -21,5 +21,5 @@ Searches files using powerful regex matching.
21
21
  - You MUST use the built-in `search` tool for any content search. NEVER shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any other CLI search via Bash — even for a single match, even "just to check quickly", even piped through other commands.
22
22
  - Bash `grep`/`rg` loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The `search` tool is faster, structured, and already wired into the workspace — there is no scenario where Bash search is preferable.
23
23
  - If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the lookup through the `search` tool instead.
24
- - If the search is open-ended, requiring multiple rounds, you MUST use the Task tool with the explore subagent instead of chaining `search` calls yourself.
24
+ - If the search is open-ended and requires multiple rounds across subsystems, delegate a bounded fact-finding task to an appropriate canonical role agent (`planner` for sequencing/context maps or `architect` for read-only architecture assessment) instead of chaining broad `search` calls yourself.
25
25
  </critical>
@@ -28,13 +28,12 @@ Subagents have no conversation history. Every fact, file path, and direction the
28
28
  {{/if}}
29
29
  {{#if independentMode}}- `.inheritContext`: independent mode cannot inherit parent conversation. Omit it or set `"none"`; any non-`none` value is rejected before scheduling.{{/if}}
30
30
  {{#if customSchemaEnabled}}- `schema`: JTD schema for expected structured output (do not put format rules in assignments){{/if}}
31
- - `spawnPlan` (optional): required before any batch with more than 4 tasks, and before a reviewer agent spawns `explore`; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.
31
+ - `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.
32
32
  {{#if isolationEnabled}}- `isolated`: run in isolated env; use when tasks edit overlapping files{{/if}}
33
33
  </parameters>
34
34
 
35
35
  <rules>
36
36
  - HARD runtime gate: calls with more than 4 tasks are rejected before any child launches unless `spawnPlan` is complete.
37
- - Reviewer->explore gate: a `reviewer` spawning `explore` is rejected before launch unless `spawnPlan` is complete, even for a single task.
38
37
  - NEVER assign tasks to run project-wide build/test/lint. Caller verifies after the batch.
39
38
  - **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates and formatters. You run them once at the end across the union of changed files — avoids redundant runs and racing formatter passes.
40
39
  {{#if ircEnabled}}
@@ -0,0 +1 @@
1
+ export * from "./ledger";
@@ -0,0 +1,177 @@
1
+ export type ResearchPlanConfidence = "low" | "medium" | "high";
2
+
3
+ export type ResearchEvidenceVerdict = "support" | "contradict" | "uncertain";
4
+
5
+ export interface ResearchPlanItem {
6
+ claim: string;
7
+ confidence: ResearchPlanConfidence;
8
+ unknowns: string[];
9
+ evidenceNeeded: string[];
10
+ counterexampleQueries: string[];
11
+ sourceConflictPolicy: string;
12
+ dropCondition: string;
13
+ verifierChecks: string[];
14
+ }
15
+
16
+ export interface ResearchEvidenceEntry {
17
+ claim: string;
18
+ source: string;
19
+ confidence: ResearchPlanConfidence;
20
+ verdict: ResearchEvidenceVerdict;
21
+ notes?: string;
22
+ }
23
+
24
+ export interface ResearchLedgerVerdict {
25
+ claim: string;
26
+ finalVerdict: "accepted" | "rejected" | "uncertain";
27
+ survivingSources: ResearchEvidenceEntry[];
28
+ rejectReason?: string;
29
+ unresolvedUnknowns: string[];
30
+ }
31
+
32
+ export interface ResearchPlanValidationResult {
33
+ valid: boolean;
34
+ errors: string[];
35
+ }
36
+
37
+ const CONFIDENCE_VALUES = new Set<ResearchPlanConfidence>(["low", "medium", "high"]);
38
+ const EVIDENCE_VERDICTS = new Set<ResearchEvidenceVerdict>(["support", "contradict", "uncertain"]);
39
+
40
+ function isNonEmptyString(value: unknown): value is string {
41
+ return typeof value === "string" && value.trim().length > 0;
42
+ }
43
+
44
+ function validateStringArray(value: unknown, field: string, minLength = 1): string[] {
45
+ if (!Array.isArray(value)) return [`${field} must be an array`];
46
+ if (value.length < minLength) return [`${field} must contain at least ${minLength} item(s)`];
47
+ return value.flatMap((item, index) =>
48
+ isNonEmptyString(item) ? [] : [`${field}[${index}] must be a non-empty string`],
49
+ );
50
+ }
51
+
52
+ export function validateResearchPlanItem(item: Partial<ResearchPlanItem>): ResearchPlanValidationResult {
53
+ const errors: string[] = [];
54
+ if (!isNonEmptyString(item.claim)) errors.push("claim must be a non-empty string");
55
+ if (!item.confidence || !CONFIDENCE_VALUES.has(item.confidence)) {
56
+ errors.push("confidence must be one of: low, medium, high");
57
+ }
58
+ errors.push(...validateStringArray(item.unknowns, "unknowns", 0));
59
+ errors.push(...validateStringArray(item.evidenceNeeded, "evidenceNeeded"));
60
+ errors.push(...validateStringArray(item.counterexampleQueries, "counterexampleQueries"));
61
+ if (!isNonEmptyString(item.sourceConflictPolicy)) errors.push("sourceConflictPolicy must be a non-empty string");
62
+ if (!isNonEmptyString(item.dropCondition)) errors.push("dropCondition must be a non-empty string");
63
+ errors.push(...validateStringArray(item.verifierChecks, "verifierChecks"));
64
+ return { valid: errors.length === 0, errors };
65
+ }
66
+
67
+ export function validateResearchEvidenceEntry(entry: Partial<ResearchEvidenceEntry>): ResearchPlanValidationResult {
68
+ const errors: string[] = [];
69
+ if (!isNonEmptyString(entry.claim)) errors.push("claim must be a non-empty string");
70
+ if (!isNonEmptyString(entry.source)) errors.push("source must be a non-empty string");
71
+ if (!entry.confidence || !CONFIDENCE_VALUES.has(entry.confidence)) {
72
+ errors.push("confidence must be one of: low, medium, high");
73
+ }
74
+ if (!entry.verdict || !EVIDENCE_VERDICTS.has(entry.verdict)) {
75
+ errors.push("verdict must be one of: support, contradict, uncertain");
76
+ }
77
+ return { valid: errors.length === 0, errors };
78
+ }
79
+
80
+ function lower(value: string): string {
81
+ return value.toLowerCase();
82
+ }
83
+
84
+ function matchesDropCondition(item: ResearchPlanItem, evidence: ResearchEvidenceEntry[]): string | undefined {
85
+ const condition = lower(item.dropCondition);
86
+ const contradiction = evidence.find(entry => entry.verdict === "contradict");
87
+ if (contradiction && /(counterexample|contradict|conflict|falsif)/.test(condition)) {
88
+ return `dropCondition matched by contradictory source: ${contradiction.source}`;
89
+ }
90
+ const unresolved = evidence.find(entry => entry.verdict === "uncertain");
91
+ if (unresolved && /(unknown|unresolved|uncertain)/.test(condition)) {
92
+ return `dropCondition matched by unresolved evidence: ${unresolved.source}`;
93
+ }
94
+ return undefined;
95
+ }
96
+
97
+ function sourceConflictReason(item: ResearchPlanItem, evidence: ResearchEvidenceEntry[]): string | undefined {
98
+ const supporting = evidence.filter(entry => entry.verdict === "support");
99
+ const contradicting = evidence.filter(entry => entry.verdict === "contradict");
100
+ if (supporting.length === 0 || contradicting.length === 0) return undefined;
101
+ const policy = lower(item.sourceConflictPolicy);
102
+ if (/(reject|drop|do not accept|prefer contradiction|requires resolution)/.test(policy)) {
103
+ return `sourceConflictPolicy rejected mixed support/contradiction (${supporting.length} support, ${contradicting.length} contradict)`;
104
+ }
105
+ return "source conflict remains unresolved";
106
+ }
107
+
108
+ export function evaluateResearchLedger(
109
+ item: ResearchPlanItem,
110
+ evidence: readonly ResearchEvidenceEntry[],
111
+ ): ResearchLedgerVerdict {
112
+ const relevantEvidence = evidence.filter(entry => entry.claim === item.claim);
113
+ const invalidItem = validateResearchPlanItem(item);
114
+ if (!invalidItem.valid) {
115
+ return {
116
+ claim: item.claim,
117
+ finalVerdict: "rejected",
118
+ survivingSources: [],
119
+ rejectReason: `invalid research plan item: ${invalidItem.errors.join("; ")}`,
120
+ unresolvedUnknowns: item.unknowns,
121
+ };
122
+ }
123
+ const invalidEvidence = relevantEvidence.flatMap(entry => validateResearchEvidenceEntry(entry).errors);
124
+ if (invalidEvidence.length > 0) {
125
+ return {
126
+ claim: item.claim,
127
+ finalVerdict: "rejected",
128
+ survivingSources: [],
129
+ rejectReason: `invalid evidence entry: ${invalidEvidence.join("; ")}`,
130
+ unresolvedUnknowns: item.unknowns,
131
+ };
132
+ }
133
+ if (relevantEvidence.length === 0) {
134
+ return {
135
+ claim: item.claim,
136
+ finalVerdict: "uncertain",
137
+ survivingSources: [],
138
+ rejectReason: "no evidence collected for claim",
139
+ unresolvedUnknowns: item.unknowns,
140
+ };
141
+ }
142
+ const supporting = relevantEvidence.filter(entry => entry.verdict === "support");
143
+ const firstContradiction = relevantEvidence.find(entry => entry.verdict === "contradict");
144
+ let dropReason = matchesDropCondition(item, relevantEvidence) ?? sourceConflictReason(item, relevantEvidence);
145
+ // A counterexample with no surviving support falsifies the claim regardless of how the
146
+ // dropCondition / sourceConflictPolicy prose is worded. Without this, a purely contradicted
147
+ // claim would slip through as "uncertain" and reopen the hallucination survival path the
148
+ // evidence ledger exists to close (a contested claim already rejects via sourceConflictReason).
149
+ if (!dropReason && firstContradiction && supporting.length === 0) {
150
+ dropReason = `claim contradicted by counterexample with no supporting evidence: ${firstContradiction.source}`;
151
+ }
152
+ if (dropReason) {
153
+ return {
154
+ claim: item.claim,
155
+ finalVerdict: "rejected",
156
+ survivingSources: supporting,
157
+ rejectReason: dropReason,
158
+ unresolvedUnknowns: item.unknowns,
159
+ };
160
+ }
161
+ const uncertain = relevantEvidence.some(entry => entry.verdict === "uncertain");
162
+ if (uncertain || supporting.length === 0) {
163
+ return {
164
+ claim: item.claim,
165
+ finalVerdict: "uncertain",
166
+ survivingSources: supporting,
167
+ rejectReason: uncertain ? "unresolved uncertainty remains" : "no supporting evidence survived verification",
168
+ unresolvedUnknowns: item.unknowns,
169
+ };
170
+ }
171
+ return {
172
+ claim: item.claim,
173
+ finalVerdict: "accepted",
174
+ survivingSources: supporting,
175
+ unresolvedUnknowns: [],
176
+ };
177
+ }
@@ -1,12 +1,17 @@
1
1
  /**
2
- * RLM session artifact layout under <cwd>/.skc/rlm/<sessionId>/.
2
+ * RLM session artifact layout under <cwd>/.skc/_session-{skcSessionId}/rlm/<rlmSessionId>/.
3
+ *
4
+ * The SKC session id (process boundary) scopes the directory; the RLM session id
5
+ * names the individual research run within it. The two ids are kept distinct.
3
6
  */
4
7
  import * as fs from "node:fs/promises";
5
8
  import * as path from "node:path";
6
9
  import { readNotebookDocument } from "../edit/notebook";
10
+ import { rlmArtifactRoot } from "../skc-runtime/session-layout";
11
+ import { resolveSkcSessionForWrite } from "../skc-runtime/session-resolution";
7
12
  import type { RlmArtifactPaths } from "./types";
8
13
 
9
- export const RLM_DIR_SEGMENT = path.join(".skc", "rlm");
14
+ export const RLM_DIR_SEGMENT = "rlm";
10
15
 
11
16
  const SESSION_ID_RE = /^[A-Za-z0-9_-]+$/;
12
17
 
@@ -25,7 +30,11 @@ export function resolveRlmArtifactPaths(cwd: string, sessionId: string): RlmArti
25
30
  if (!isValidRlmSessionId(sessionId)) {
26
31
  throw new Error(`Invalid RLM session id: ${JSON.stringify(sessionId)}`);
27
32
  }
28
- const dir = path.join(cwd, RLM_DIR_SEGMENT, sessionId);
33
+ const dir = rlmArtifactRoot(
34
+ cwd,
35
+ resolveSkcSessionForWrite(cwd, { envSessionId: process.env.SKC_SESSION_ID }).skcSessionId,
36
+ sessionId,
37
+ );
29
38
  return {
30
39
  dir,
31
40
  notebookPath: path.join(dir, "notebook.ipynb"),
package/src/rlm/index.ts CHANGED
@@ -15,6 +15,7 @@ import { type RlmPreset, runRootCommand } from "../main";
15
15
  import rlmReportCommandPrompt from "../prompts/system/rlm-report-command.md" with { type: "text" };
16
16
  import type { CreateAgentSessionOptions } from "../sdk";
17
17
  import type { AgentSession } from "../session/agent-session";
18
+ import { resolveSessionIdFromSources, writeSessionActivityMarker } from "../skc-runtime/session-resolution";
18
19
  import {
19
20
  ensureRlmSessionDir,
20
21
  generateRlmSessionId,
@@ -231,6 +232,12 @@ async function writeRlmMetadata(input: {
231
232
  successfulRuns: input.successfulRuns,
232
233
  };
233
234
  await Bun.write(input.paths.metadataPath, `${JSON.stringify(metadata, null, 2)}\n`);
235
+ // Best-effort: update the per-session activity marker so latest-session auto-detect
236
+ // accounts for RLM-only generated output (AC2). Never let marker failure break RLM.
237
+ const skcSessionId = resolveSessionIdFromSources({ envSessionId: process.env.SKC_SESSION_ID })?.skcSessionId;
238
+ if (skcSessionId) {
239
+ await writeSessionActivityMarker(input.cwd, skcSessionId, { writer: "rlm" }).catch(() => {});
240
+ }
234
241
  }
235
242
 
236
243
  export async function runRlmCommand(argv: string[]): Promise<void> {
@@ -149,6 +149,52 @@ export async function updateMCPServer(filePath: string, name: string, config: MC
149
149
  await writeMCPConfigFile(filePath, updated);
150
150
  }
151
151
 
152
+ /**
153
+ * Result of an {@link upsertMCPServer} call.
154
+ * - `added`: server did not exist and was written.
155
+ * - `updated`: server existed and was overwritten because `force` was set.
156
+ * - `skipped`: server existed and `force` was not set, so nothing was written.
157
+ */
158
+ export type UpsertMCPServerResult =
159
+ | { status: "added" }
160
+ | { status: "updated" }
161
+ | { status: "skipped"; reason: "exists" };
162
+
163
+ /**
164
+ * Add an MCP server, or overwrite an existing one only when `force` is set.
165
+ *
166
+ * Collision-aware wrapper over {@link addMCPServer} / {@link updateMCPServer} used by
167
+ * `skc migrate`. Never connects to the server. Reuses the underlying writers so the
168
+ * rest of the config file (including `disabledServers`) is preserved on update.
169
+ *
170
+ * @throws Error if the server name or config is invalid (validated before any write).
171
+ */
172
+ export async function upsertMCPServer(
173
+ filePath: string,
174
+ name: string,
175
+ config: MCPServerConfig,
176
+ options: { force?: boolean } = {},
177
+ ): Promise<UpsertMCPServerResult> {
178
+ // Validate name up front so an invalid name fails regardless of collision state.
179
+ const nameError = validateServerName(name);
180
+ if (nameError) {
181
+ throw new Error(nameError);
182
+ }
183
+
184
+ const existing = await getMCPServer(filePath, name);
185
+ if (existing) {
186
+ if (!options.force) {
187
+ return { status: "skipped", reason: "exists" };
188
+ }
189
+ // updateMCPServer preserves the rest of MCPConfigFile, incl. disabledServers.
190
+ await updateMCPServer(filePath, name, config);
191
+ return { status: "updated" };
192
+ }
193
+
194
+ await addMCPServer(filePath, name, config);
195
+ return { status: "added" };
196
+ }
197
+
152
198
  /**
153
199
  * Remove an MCP server from a config file.
154
200
  *