@linzumi/cli 0.0.86-beta → 0.0.88-beta

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +534 -69
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -58,7 +58,7 @@ Install the CLI or run it with `npx`:
58
58
  ```bash
59
59
  npm install -g @linzumi/cli@latest
60
60
  npx -y @linzumi/cli@latest signup
61
- npx -y @linzumi/cli@0.0.86-beta --version
61
+ npx -y @linzumi/cli@0.0.88-beta --version
62
62
  linzumi --version
63
63
  ```
64
64
 
package/dist/index.js CHANGED
@@ -11541,7 +11541,7 @@ function createClaudeCodeSessionPipeline(host) {
11541
11541
  const toolName2 = pending?.toolName ?? event.toolName ?? "tool";
11542
11542
  const input = pending?.input ?? {};
11543
11543
  const itemId = `tool:${event.itemKey}`;
11544
- if (!event.isError && isFileChangeTool(toolName2)) {
11544
+ if (isFileChangeTool(toolName2)) {
11545
11545
  const patchText = claudeFileChangePatchText(toolName2, input, {
11546
11546
  targetExistedAtCall: pending?.targetExistedAtCall
11547
11547
  });
@@ -11549,7 +11549,7 @@ function createClaudeCodeSessionPipeline(host) {
11549
11549
  const item2 = {
11550
11550
  type: "fileChange",
11551
11551
  id: itemId,
11552
- status: "completed",
11552
+ status: event.isError ? "failed" : "completed",
11553
11553
  patchText
11554
11554
  };
11555
11555
  recordItem(turn, item2);
@@ -11569,12 +11569,13 @@ function createClaudeCodeSessionPipeline(host) {
11569
11569
  submit("item/completed", { turnId: turn.turnId, item: item2 });
11570
11570
  return;
11571
11571
  }
11572
- if (!event.isError && toolName2 === "TodoWrite") {
11572
+ if (!event.isError && isClaudePlanTool(toolName2)) {
11573
11573
  host.onTodoWriteCompleted?.(input);
11574
11574
  const plan = claudePlanItem(itemId, input);
11575
11575
  if (plan !== void 0) {
11576
- recordItem(turn, plan);
11577
- submit("item/completed", { turnId: turn.turnId, item: plan });
11576
+ const planRow = { ...plan, id: itemId };
11577
+ recordItem(turn, planRow);
11578
+ submit("item/completed", { turnId: turn.turnId, item: planRow });
11578
11579
  return;
11579
11580
  }
11580
11581
  }
@@ -11605,7 +11606,7 @@ function createClaudeCodeSessionPipeline(host) {
11605
11606
  }
11606
11607
  case "reasoning_delta": {
11607
11608
  const turn = ensureTurn();
11608
- const itemId = claudeAssistantPipelineItemId(event.itemKey);
11609
+ const itemId = claudeReasoningPipelineItemId(event.itemKey);
11609
11610
  appendRawText(turn, itemId, "reasoning", event.delta);
11610
11611
  submit("item/reasoning/textDelta", {
11611
11612
  turnId: turn.turnId,
@@ -11626,7 +11627,7 @@ function createClaudeCodeSessionPipeline(host) {
11626
11627
  }
11627
11628
  case "reasoning_message": {
11628
11629
  const turn = ensureTurn();
11629
- const itemId = claudeAssistantPipelineItemId(event.itemKey);
11630
+ const itemId = claudeReasoningPipelineItemId(event.itemKey);
11630
11631
  setRawText(turn, itemId, "reasoning", event.content);
11631
11632
  submit("item/completed", {
11632
11633
  turnId: turn.turnId,
@@ -11636,14 +11637,15 @@ function createClaudeCodeSessionPipeline(host) {
11636
11637
  }
11637
11638
  case "tool_call": {
11638
11639
  const turn = ensureTurn();
11640
+ const targetExistedAtCall = fileChangeTargetExists(
11641
+ event.toolName,
11642
+ event.input,
11643
+ host.cwd
11644
+ );
11639
11645
  turn.pendingTools.set(event.itemKey, {
11640
11646
  toolName: event.toolName,
11641
11647
  input: event.input,
11642
- targetExistedAtCall: fileChangeTargetExists(
11643
- event.toolName,
11644
- event.input,
11645
- host.cwd
11646
- )
11648
+ targetExistedAtCall
11647
11649
  });
11648
11650
  if (isWebSearchTool(event.toolName)) {
11649
11651
  const query = stringValue(event.input.query) ?? stringValue(event.input.url) ?? event.toolName;
@@ -11655,7 +11657,34 @@ function createClaudeCodeSessionPipeline(host) {
11655
11657
  query
11656
11658
  }
11657
11659
  });
11660
+ return;
11658
11661
  }
11662
+ if (isClaudePlanTool(event.toolName)) {
11663
+ return;
11664
+ }
11665
+ const itemId = `tool:${event.itemKey}`;
11666
+ if (isFileChangeTool(event.toolName)) {
11667
+ const patchText = claudeFileChangePatchText(
11668
+ event.toolName,
11669
+ event.input,
11670
+ { targetExistedAtCall }
11671
+ );
11672
+ if (patchText !== void 0 && patchText.trim() !== "") {
11673
+ submit("item/fileChange/outputDelta", {
11674
+ turnId: turn.turnId,
11675
+ itemId,
11676
+ delta: patchText
11677
+ });
11678
+ }
11679
+ return;
11680
+ }
11681
+ submit("item/commandExecution/outputDelta", {
11682
+ turnId: turn.turnId,
11683
+ itemId,
11684
+ command: claudeToolCommandLabel(event.toolName, event.input),
11685
+ stream: "stdout",
11686
+ delta: "\n"
11687
+ });
11659
11688
  return;
11660
11689
  }
11661
11690
  case "tool_result": {
@@ -11809,6 +11838,9 @@ function claudeAssistantPipelineItemId(itemKey) {
11809
11838
  const streamIndex = itemKey.match(/^assistant-stream-(\d+)$/)?.[1];
11810
11839
  return streamIndex === void 0 ? itemKey : `content-block-${streamIndex}`;
11811
11840
  }
11841
+ function claudeReasoningPipelineItemId(itemKey) {
11842
+ return `reasoning-${claudeAssistantPipelineItemId(itemKey)}`;
11843
+ }
11812
11844
  function isFileChangeTool(toolName2) {
11813
11845
  switch (toolName2) {
11814
11846
  case "Edit":
@@ -11823,6 +11855,9 @@ function isFileChangeTool(toolName2) {
11823
11855
  function isWebSearchTool(toolName2) {
11824
11856
  return toolName2 === "WebSearch" || toolName2 === "WebFetch";
11825
11857
  }
11858
+ function isClaudePlanTool(toolName2) {
11859
+ return toolName2 === "TodoWrite" || toolName2 === "TaskCreate" || toolName2 === "TaskUpdate";
11860
+ }
11826
11861
  function fileChangeTargetExists(toolName2, input, sessionCwd) {
11827
11862
  if (!isFileChangeTool(toolName2)) {
11828
11863
  return void 0;
@@ -11895,9 +11930,18 @@ ${body}
11895
11930
  *** End Patch
11896
11931
  `;
11897
11932
  }
11933
+ function claudePlanInputItems(input) {
11934
+ if (Array.isArray(input.todos)) {
11935
+ return input.todos;
11936
+ }
11937
+ if (Array.isArray(input.tasks)) {
11938
+ return input.tasks;
11939
+ }
11940
+ return [];
11941
+ }
11898
11942
  function claudePlanItem(itemId, input) {
11899
- const todos = Array.isArray(input.todos) ? input.todos : [];
11900
- const lines = todos.flatMap((todo) => {
11943
+ const items = claudePlanInputItems(input);
11944
+ const lines = items.flatMap((todo) => {
11901
11945
  const value = objectValue(todo);
11902
11946
  if (value === void 0) {
11903
11947
  return [];
@@ -11971,7 +12015,7 @@ function objectValue2(value) {
11971
12015
  return isJsonObject(value) ? value : void 0;
11972
12016
  }
11973
12017
  function claudeTodoWritePlanSteps(input) {
11974
- const todos = Array.isArray(input.todos) ? input.todos : [];
12018
+ const todos = Array.isArray(input.todos) ? input.todos : Array.isArray(input.tasks) ? input.tasks : [];
11975
12019
  const steps = todos.flatMap((todo) => {
11976
12020
  const value = objectValue2(todo);
11977
12021
  if (value === void 0) {
@@ -12042,6 +12086,29 @@ function createClaudeCodePlanMirror(args) {
12042
12086
  settle: () => flight
12043
12087
  };
12044
12088
  }
12089
+ async function seedClaudeCodingJobGoal(args) {
12090
+ const goal = args.goal.trim().slice(0, 2e3);
12091
+ if (goal === "") {
12092
+ return false;
12093
+ }
12094
+ try {
12095
+ await args.upsertCodingJobPlan(
12096
+ { thread_id: args.threadId, goal },
12097
+ { signal: args.signal }
12098
+ );
12099
+ args.log("claude_code.goal_seeded", {
12100
+ thread_id: args.threadId,
12101
+ goal_length: goal.length
12102
+ });
12103
+ return true;
12104
+ } catch (error) {
12105
+ args.log("claude_code.goal_seed_failed", {
12106
+ thread_id: args.threadId,
12107
+ message: error instanceof Error ? error.message : String(error)
12108
+ });
12109
+ return false;
12110
+ }
12111
+ }
12045
12112
  var todoStatusToPlanStepStatus, planStepTitleMaxLength, planStepDescriptionMaxLength, planStepsMaxCount;
12046
12113
  var init_claudeCodePlanMirror = __esm({
12047
12114
  "src/claudeCodePlanMirror.ts"() {
@@ -12597,6 +12664,87 @@ var init_claudeCodeLiveBashOutput = __esm({
12597
12664
  }
12598
12665
  });
12599
12666
 
12667
+ // src/claudeCodeTurnStallWatchdog.ts
12668
+ function createClaudeCodeTurnStallWatchdog(options) {
12669
+ if (!(options.thresholdMs > 0)) {
12670
+ throw new Error("Claude turn-stall threshold must be > 0");
12671
+ }
12672
+ const now = options.now ?? (() => Date.now());
12673
+ const setTimer = options.setTimer ?? ((callback, delayMs) => {
12674
+ const handle2 = setTimeout(callback, delayMs);
12675
+ handle2.unref?.();
12676
+ return handle2;
12677
+ });
12678
+ const clearTimer = options.clearTimer ?? ((handle2) => clearTimeout(handle2));
12679
+ const isTurnActive = options.isTurnActive ?? (() => true);
12680
+ let handle;
12681
+ let lastKickMs = now();
12682
+ let stopped = false;
12683
+ let fired = false;
12684
+ let wasActive = isTurnActive();
12685
+ const disarm = () => {
12686
+ if (handle !== void 0) {
12687
+ clearTimer(handle);
12688
+ handle = void 0;
12689
+ }
12690
+ };
12691
+ const onTimeout = () => {
12692
+ handle = void 0;
12693
+ if (stopped || fired) {
12694
+ return;
12695
+ }
12696
+ const active = isTurnActive();
12697
+ if (!active) {
12698
+ wasActive = false;
12699
+ arm();
12700
+ return;
12701
+ }
12702
+ if (!wasActive) {
12703
+ wasActive = true;
12704
+ lastKickMs = now();
12705
+ arm();
12706
+ return;
12707
+ }
12708
+ const silentMs = now() - lastKickMs;
12709
+ if (silentMs < options.thresholdMs) {
12710
+ arm();
12711
+ return;
12712
+ }
12713
+ fired = true;
12714
+ options.onStall({ silentMs });
12715
+ };
12716
+ function arm() {
12717
+ if (stopped || fired) {
12718
+ return;
12719
+ }
12720
+ disarm();
12721
+ const delay = isTurnActive() ? Math.max(0, options.thresholdMs - (now() - lastKickMs)) : options.thresholdMs;
12722
+ handle = setTimer(onTimeout, delay);
12723
+ }
12724
+ arm();
12725
+ return {
12726
+ kick: () => {
12727
+ if (stopped || fired) {
12728
+ return;
12729
+ }
12730
+ wasActive = true;
12731
+ lastKickMs = now();
12732
+ arm();
12733
+ },
12734
+ stop: () => {
12735
+ stopped = true;
12736
+ disarm();
12737
+ }
12738
+ };
12739
+ }
12740
+ var defaultClaudeCodeTurnStallThresholdMs;
12741
+ var init_claudeCodeTurnStallWatchdog = __esm({
12742
+ "src/claudeCodeTurnStallWatchdog.ts"() {
12743
+ "use strict";
12744
+ defaultClaudeCodeTurnStallThresholdMs = 10 * 6e4;
12745
+ }
12746
+ });
12747
+
12600
12748
  // src/claudeCodeSession.ts
12601
12749
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
12602
12750
  import { homedir as homedir6 } from "node:os";
@@ -12864,8 +13012,49 @@ async function startClaudeCodeSession(options) {
12864
13012
  lastCompletedTurnBody: void 0
12865
13013
  };
12866
13014
  const streamUsageTracker = createClaudeCodeStreamUsageTracker();
13015
+ let stallReason;
13016
+ let signalStall;
13017
+ const stallSignal = new Promise((resolve12) => {
13018
+ signalStall = resolve12;
13019
+ });
13020
+ const stallWatchdog = options.turnStallThresholdMs !== void 0 && options.turnStallThresholdMs > 0 ? createClaudeCodeTurnStallWatchdog({
13021
+ thresholdMs: options.turnStallThresholdMs,
13022
+ isTurnActive: options.turnStallIsActive,
13023
+ onStall: ({ silentMs }) => {
13024
+ stallReason = `Claude Code turn stalled: no SDK output for ${Math.round(
13025
+ silentMs / 1e3
13026
+ )}s (threshold ${Math.round(
13027
+ (options.turnStallThresholdMs ?? 0) / 1e3
13028
+ )}s)`;
13029
+ options.abortController?.abort();
13030
+ signalStall?.();
13031
+ }
13032
+ }) : void 0;
12867
13033
  try {
12868
- for await (const message of runner(options)) {
13034
+ const iterator = runner(options)[Symbol.asyncIterator]();
13035
+ for (; ; ) {
13036
+ let nextStep;
13037
+ if (stallWatchdog) {
13038
+ const nextPromise = iterator.next().then((result) => ({ kind: "next", result }));
13039
+ nextPromise.catch(() => void 0);
13040
+ const raced = await Promise.race([
13041
+ nextPromise,
13042
+ stallSignal.then(() => ({ kind: "stall" }))
13043
+ ]);
13044
+ if (raced.kind === "stall") {
13045
+ void Promise.resolve(iterator.return?.()).catch(() => void 0);
13046
+ break;
13047
+ }
13048
+ nextStep = raced;
13049
+ } else {
13050
+ nextStep = { kind: "next", result: await iterator.next() };
13051
+ }
13052
+ const step = nextStep;
13053
+ if (step.result.done === true) {
13054
+ break;
13055
+ }
13056
+ const message = step.result.value;
13057
+ stallWatchdog?.kick();
12869
13058
  state.sessionId = state.sessionId ?? extractClaudeSessionId(message);
12870
13059
  const sessionId = extractClaudeSessionId(message) ?? state.sessionId;
12871
13060
  if (sessionId !== void 0 && !state.startedSessionIds.has(sessionId)) {
@@ -12931,8 +13120,12 @@ async function startClaudeCodeSession(options) {
12931
13120
  }
12932
13121
  }
12933
13122
  } finally {
13123
+ stallWatchdog?.stop();
12934
13124
  options.streamingInput?.close();
12935
13125
  }
13126
+ if (stallReason !== void 0) {
13127
+ return await failClaudeCodeSession(options, state.sessionId, stallReason);
13128
+ }
12936
13129
  return completeClaudeCodeSession(options, state);
12937
13130
  }
12938
13131
  async function emitClaudeCodeTurnCompleted(options, state) {
@@ -13073,7 +13266,16 @@ async function* defaultClaudeCodeRunner(options) {
13073
13266
  options.wrapApprovedToolInput
13074
13267
  )
13075
13268
  },
13076
- ...options.model === void 0 ? {} : { model: options.model }
13269
+ ...options.model === void 0 ? {} : { model: options.model },
13270
+ ...options.permissionMode === void 0 ? {} : {
13271
+ permissionMode: options.permissionMode,
13272
+ // The SDK requires this safety acknowledgement when a session is
13273
+ // constructed in 'bypassPermissions' (sdk.d.ts Options:
13274
+ // "Must be set to true when using permissionMode:
13275
+ // 'bypassPermissions'"). It is the intentional-bypass flag, only
13276
+ // meaningful for that mode; harmless for the others.
13277
+ ...options.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}
13278
+ }
13077
13279
  // exactOptionalPropertyTypes: the conditional spreads build a union the
13078
13280
  // SDK's Options cannot absorb verbatim; the shape is correct field-wise.
13079
13281
  }
@@ -13316,13 +13518,16 @@ function assistantTranscriptEvents(message, sessionId) {
13316
13518
  contentBlockItemKey(block, index)
13317
13519
  )
13318
13520
  );
13319
- const directText = nonEmptyText(stringValue(message?.text));
13521
+ const hasTextContentBlock = content.some(
13522
+ (block) => stringValue(objectValue(block)?.type) === "text"
13523
+ );
13524
+ const directText = hasTextContentBlock ? void 0 : nonEmptyText(stringValue(message?.text));
13320
13525
  return directText === void 0 ? contentEvents : [
13321
13526
  ...contentEvents,
13322
13527
  {
13323
13528
  type: "assistant_message",
13324
13529
  sessionId,
13325
- itemKey: stringValue(message?.uuid) ?? "assistant-message",
13530
+ itemKey: `content-block-${content.length}`,
13326
13531
  content: directText,
13327
13532
  streamState: "completed"
13328
13533
  }
@@ -13343,7 +13548,7 @@ function userTranscriptEvents(message, sessionId) {
13343
13548
  {
13344
13549
  type: "tool_result",
13345
13550
  sessionId,
13346
- itemKey: contentBlockItemKey(block, index),
13551
+ itemKey: stringValue(value?.tool_use_id) ?? stringValue(value?.id) ?? `content-block-${index}`,
13347
13552
  toolName: void 0,
13348
13553
  content: visibleToolResultContent(value),
13349
13554
  isError: value?.is_error === true
@@ -13429,12 +13634,14 @@ function unknownTranscriptEvent(message, sessionId) {
13429
13634
  }
13430
13635
  function streamItemKey(event, _message) {
13431
13636
  const index = integerValue(event?.index);
13432
- const contentBlock = objectValue(event?.content_block);
13433
- return stringValue(event?.content_block_id) ?? stringValue(contentBlock?.id) ?? stringValue(contentBlock?.tool_use_id) ?? stringValue(contentBlock?.uuid) ?? (index === void 0 ? "assistant-stream" : `assistant-stream-${index}`);
13637
+ return index === void 0 ? "assistant-stream" : `content-block-${index}`;
13434
13638
  }
13435
13639
  function contentBlockItemKey(block, index) {
13436
13640
  const value = objectValue(block);
13437
- return stringValue(value?.id) ?? stringValue(value?.tool_use_id) ?? stringValue(value?.uuid) ?? `content-block-${index}`;
13641
+ if (stringValue(value?.type) === "tool_use") {
13642
+ return stringValue(value?.id) ?? stringValue(value?.tool_use_id) ?? `content-block-${index}`;
13643
+ }
13644
+ return `content-block-${index}`;
13438
13645
  }
13439
13646
  function visibleToolResultContent(block) {
13440
13647
  const content = block?.content;
@@ -13482,6 +13689,7 @@ var init_claudeCodeSession = __esm({
13482
13689
  "use strict";
13483
13690
  init_json();
13484
13691
  init_claudeCodeLiveBashOutput();
13692
+ init_claudeCodeTurnStallWatchdog();
13485
13693
  claudeRateLimitWindowLabels = {
13486
13694
  five_hour: "5h window",
13487
13695
  seven_day: "7d window",
@@ -14992,8 +15200,8 @@ function startCallbackServer(args) {
14992
15200
  }
14993
15201
  resolveCallback?.({ code, state });
14994
15202
  writeOauthResult(response, {
14995
- title: "Connected",
14996
- body: "You can close this tab. Your terminal will finish the setup.",
15203
+ title: "Computer authorized",
15204
+ body: "This computer has been authorized for agentic use in Linzumi. Feel free to close this tab and let the terminal finish setup.",
14997
15205
  status: 200
14998
15206
  });
14999
15207
  } catch (error) {
@@ -15036,29 +15244,108 @@ function writeOauthResult(response, args) {
15036
15244
  response.end(oauthResultHtml(args));
15037
15245
  }
15038
15246
  function oauthResultHtml(args) {
15247
+ const success = args.status >= 200 && args.status < 300;
15248
+ const badgeClass = success ? "badge badge--success" : "badge badge--notice";
15249
+ const badgeMark = success ? '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" class="mark"><path fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d="M5 12.5l4.2 4.2L19 7"/></svg>' : '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" class="mark"><path fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d="M12 7v6M12 17h.01"/></svg>';
15039
15250
  return `<!doctype html>
15040
- <html>
15251
+ <html lang="en">
15041
15252
  <head>
15042
15253
  <meta charset="utf-8" />
15043
15254
  <meta name="viewport" content="width=device-width, initial-scale=1" />
15044
15255
  <title>${escapeHtml(args.title)}</title>
15045
15256
  <style>
15046
- :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
15047
- body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f6f7f9; color: #13161c; }
15048
- main { width: min(520px, calc(100vw - 32px)); border: 1px solid #d9dde5; border-radius: 12px; background: #fff; padding: 30px; box-shadow: 0 24px 70px rgba(18, 25, 38, 0.12); }
15049
- h1 { margin: 0 0 10px; font-size: 24px; line-height: 1.2; }
15050
- p { margin: 0; color: #465160; line-height: 1.55; }
15257
+ :root {
15258
+ color-scheme: light dark;
15259
+ /* Linzumi brand tokens (light) \u2014 kandan/server_v2/web/src/ui/theme/tokens.css */
15260
+ --page-bg: #f5f2ee;
15261
+ --surface: #fcfaf7;
15262
+ --foreground: #1e1b18;
15263
+ --muted: #5c5550;
15264
+ --divider: rgba(138, 148, 144, 0.16);
15265
+ --accent: #c4897a;
15266
+ --success: #5a9a6a;
15267
+ --success-soft: rgba(90, 154, 106, 0.1);
15268
+ --notice-soft: rgba(196, 137, 122, 0.1);
15269
+ --shadow: 0 12px 40px rgba(106, 138, 122, 0.12), 0 4px 12px rgba(106, 138, 122, 0.06);
15270
+ }
15051
15271
  @media (prefers-color-scheme: dark) {
15052
- body { background: #0f131a; color: #f3f5f8; }
15053
- main { background: #171c25; border-color: #2b3442; box-shadow: 0 24px 70px rgba(0, 0, 0, 0.4); }
15054
- p { color: #aab4c2; }
15272
+ :root {
15273
+ /* Linzumi brand tokens (dark) */
15274
+ --page-bg: #171b19;
15275
+ --surface: #202522;
15276
+ --foreground: rgba(245, 241, 232, 0.95);
15277
+ --muted: rgba(245, 241, 232, 0.68);
15278
+ --divider: rgba(245, 241, 232, 0.1);
15279
+ --accent: #c8a0b0;
15280
+ --success: #72c488;
15281
+ --success-soft: rgba(114, 196, 136, 0.14);
15282
+ --notice-soft: rgba(200, 160, 176, 0.14);
15283
+ --shadow: 0 18px 50px rgba(0, 0, 0, 0.5), 0 4px 12px rgba(0, 0, 0, 0.35);
15284
+ }
15285
+ }
15286
+ * { box-sizing: border-box; }
15287
+ body {
15288
+ margin: 0;
15289
+ min-height: 100vh;
15290
+ display: grid;
15291
+ place-items: center;
15292
+ padding: 24px;
15293
+ background:
15294
+ radial-gradient(1200px 600px at 50% -10%, var(--notice-soft), transparent 60%),
15295
+ var(--page-bg);
15296
+ color: var(--foreground);
15297
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
15298
+ -webkit-font-smoothing: antialiased;
15299
+ }
15300
+ main {
15301
+ width: min(520px, 100%);
15302
+ border: 1px solid var(--divider);
15303
+ border-radius: 16px;
15304
+ background: var(--surface);
15305
+ padding: 40px 36px;
15306
+ box-shadow: var(--shadow);
15307
+ text-align: center;
15308
+ }
15309
+ .logo {
15310
+ width: 44px;
15311
+ height: 44px;
15312
+ margin: 28px auto 0;
15313
+ color: var(--accent);
15314
+ }
15315
+ .logo svg { display: block; width: 100%; height: 100%; }
15316
+ .badge {
15317
+ width: 64px;
15318
+ height: 64px;
15319
+ margin: 0 auto 22px;
15320
+ display: grid;
15321
+ place-items: center;
15322
+ border-radius: 999px;
15323
+ }
15324
+ .badge--success { background: var(--success-soft); color: var(--success); }
15325
+ .badge--notice { background: var(--notice-soft); color: var(--accent); }
15326
+ .mark { width: 32px; height: 32px; }
15327
+ h1 {
15328
+ margin: 0 0 12px;
15329
+ font-size: 23px;
15330
+ font-weight: 600;
15331
+ line-height: 1.3;
15332
+ letter-spacing: -0.01em;
15333
+ color: var(--foreground);
15334
+ }
15335
+ p {
15336
+ margin: 0;
15337
+ color: var(--muted);
15338
+ font-size: 15px;
15339
+ line-height: 1.6;
15055
15340
  }
15056
15341
  </style>
15057
15342
  </head>
15058
15343
  <body>
15059
15344
  <main>
15345
+ <div class="${badgeClass}">${badgeMark}</div>
15060
15346
  <h1>${escapeHtml(args.title)}</h1>
15061
15347
  <p>${escapeHtml(args.body)}</p>
15348
+ <div class="logo">${LINZUMI_LOGO_SVG}</div>
15062
15349
  </main>
15063
15350
  </body>
15064
15351
  </html>`;
@@ -15089,10 +15376,12 @@ function openBrowser(url) {
15089
15376
  });
15090
15377
  });
15091
15378
  }
15379
+ var LINZUMI_LOGO_SVG;
15092
15380
  var init_oauth = __esm({
15093
15381
  "src/oauth.ts"() {
15094
15382
  "use strict";
15095
15383
  init_runnerLogger();
15384
+ LINZUMI_LOGO_SVG = '<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><path fill="currentColor" d="M206.7,82.37l2.55-6.45,2.01-3.63c.67-1.21,1.07-1.88,2.55-3.63,1.34-1.61,3.36-4.43,5.64-6.58,2.42-2.24,5.06-4.3,7.93-6.18,3.92-2.53,8.17-4.52,12.63-5.91,1.79-.63,4.03-1.16,6.72-1.61,6.1-.88,12.29-.92,18.4-.13,2.69.45,5.06,1.03,7.12,1.75,2.15.67,3.49,1.07,5.64,2.15,4.55,2.16,8.87,4.77,12.9,7.79,1.61,1.34,2.42,2.15,4.16,4.16l5.91,7.66c1.34,2.01,1.34,2.28,2.15,4.03l2.55,6.58c.67,1.34-.13,1.34,1.07.94,1.21-.27,4.3-2.01,6.45-2.82,4.7-1.83,9.62-3,14.64-3.49,2.42-.27,3.76-.54,7.12-.27s9.27,1.21,12.76,2.15c3.63.94,6.58,2.55,8.73,3.49,2.15,1.07,2.55,1.48,4.16,2.55,3.49,2.51,6.77,5.29,9.81,8.33,1.52,1.7,3.04,3.58,4.57,5.64,2.74,4.49,4.86,9.32,6.31,14.37.67,2.28,1.07,3.49,1.34,6.72.27,3.22,0,10.48.4,12.76.27,2.28.27.67,1.61.94,1.48.27,4.3,0,6.72.4,2.42.27,4.97.85,7.66,1.75,6.76,2.32,13.01,5.92,18.4,10.61,2.28,1.75,2.96,2.69,4.3,4.3,3.51,3.93,6.32,8.43,8.33,13.3,1.07,2.15,1.21,2.28,1.88,5.1.54,2.96,1.61,8.19,1.61,12.36s-.81,9.67-1.34,12.76c-.67,3.22-1.07,4.03-2.15,6.18-2.16,4.54-4.81,8.82-7.93,12.76-1.25,1.61-2.6,3-4.03,4.16-1.48,1.34-2.69,2.42-4.7,3.76-3.79,2.34-7.74,4.4-11.82,6.18l-6.58,2.15c-2.06.45-4.12.72-6.18.81l-7.25.13c-2.28,0-2.82.27-6.58-.4-3.76-.81-11.69-2.82-15.99-4.3-4.16-1.34-6.31-2.69-9.67-4.43-3.49-1.75-10.48-6.18-12.9-7.39-8.26-4.54-16.78-8.58-25.52-12.09-4.3-1.75-9.27-3.22-11.82-4.03-2.55-.81-5.24-1.61-7.66-2.15-8.39-2.25-16.99-3.6-25.66-4.03-3.9-.27-4.84-.27-8.19-.13-3.67-.09-7.79.04-12.36.4-7.9.79-15.75,2.04-23.51,3.76-8.37,2.26-16.62,4.95-24.72,8.06-3.22,1.34-2.82.81-9.13,4.16-6.31,3.36-22.03,12.22-28.75,15.58-6.72,3.36-8.73,3.49-11.82,4.43-3.09,1.07-4.3,1.21-6.58,1.61-7.3,1.09-14.71,1.18-22.03.27-2.55-.54-5.78-1.61-7.79-2.28-2.01-.54-2.15-.67-4.03-1.75-2.02-1.07-5.64-3.22-7.79-4.7-3.24-2.15-6.14-4.77-8.6-7.79-1.34-1.52-2.69-3.36-4.03-5.51-1.52-2.51-2.96-5.24-4.3-8.19-2.03-5.46-3.03-11.24-2.96-17.06,0-2.96.13-6.72.67-9.67.54-2.87,1.34-5.6,2.42-8.19,4.24-10.52,12.04-19.22,22.03-24.58,5.19-2.98,10.88-4.98,16.79-5.91,2.55-.4,5.37-.13,6.72-.4,1.34-.27,1.34,1.34,1.61-.94.4-2.28,0-9.4.4-12.76.27-3.36.81-4.97,1.48-7.25,1.87-6.19,4.88-11.97,8.87-17.06,3.47-3.98,7.39-7.54,11.69-10.61,4.43-2.61,9.11-4.77,13.97-6.45,2.55-.67,6.99-1.34,9.13-1.75,2.15-.27,1.88-.13,3.63-.13,1.79-.09,4.03,0,6.72.27,2.69.18,5.73.72,9.13,1.61,3.49,1.07,9.27,4.03,11.28,4.7s.4.27,1.07-.94ZM512.27,255.7c0,141.39-114.62,256-256,256S.27,397.09.27,255.7,114.89-.3,256.27-.3s256,114.62,256,256ZM449.33,352.66c14.67-29.16,22.94-62.09,22.94-96.95,0-119.29-96.71-216-216-216S40.27,136.41,40.27,255.7c0,41.89,11.94,80.98,32.57,114.09,1.7,1.79,2.94.65,3.05-.57.27-1.61,2.75-20.85,4.43-27.27,1.87-7.6,4.52-14.98,7.93-22.03,1.75-3.49,3.76-7.93,6.18-11.82,8.77-13.95,19.75-26.38,32.51-36.81,7.24-5.32,14.87-10.08,22.84-14.24,9.74-4.78,20.09-8.22,30.76-10.21,4.03-.99,7.61-1.66,10.75-2.02,3.36-.54,5.78-.54,9.27-.94,3.49-.54,9.32-1.16,11.82-2.01,6.65-2.27,5.64-6.72,3.09-7.52-2.55-.81-9.67-1.88-13.84-2.96-4.16-1.21-8.73-2.82-11.28-3.76-2.55-.94-2.28-.94-3.9-2.02l-5.91-4.03c-.94-.94-1.34-.94.54-2.02l10.21-5.51c2.01-1.07,1.75-.67,3.09-.13,1.34.67,4.3,2.42,6.72,3.36,7.2,2.69,14.96,3.52,22.57,2.42,1.34-.27,1.34-.54,1.61-1.21.27-.67.81-.94-.13-2.69-.94-1.88-4.43-6.58-5.64-8.46l-1.21-2.28c-.27-.45-1.07-1.07,1.21-1.75,2.42-.54,10.48-2.58,12.9-2.82.86-.09.67,0,1.75,1.07,1.07,1.07,3.09,3.63,4.84,5.24,1.88,1.61,4.43,3.22,6.18,4.3,2.8,1.71,5.99,2.68,9.27,2.82,1.88,0,4.97-.13,7.12-.67,2.15-.4,3.63-.94,5.64-2.02,2.01-1.07,4.7-2.69,6.72-4.43,2.01-1.75,3.49-5.1,5.64-6.04,2.15-.81,4.97.54,7.12.94,2.28.4,4.97,1.21,6.18,1.61,1.21.4,2.01.4.67,2.28l-8.06,10.34c-1.34,1.88.13,5.51,1.21,5.64l5.64.27c2.28-.13,4.43.13,7.66-.67,3.36-.81,9.27-2.96,11.82-3.9,2.69-.94,3.22-1.34,4.16-2.01.81-.54,1.39-1.03,1.75-1.48.45-.27.67-1.07,2.82,0s9.4,4.97,11.28,6.18c2.01,1.21,1.07.94,0,1.75l-5.51,3.9c-1.48.94-1.07.94-3.22,1.88-2.15,1.07-6.58,2.96-9.67,4.03s-4.03,1.75-8.73,2.69c-4.7.81-14.51,1.75-19.48,2.55-4.97.81-7.93,1.34-10.34,2.01-2.28.67-2.15.94-3.49,1.88-1.48,1.07-2.69,1.61-4.97,3.9-2.28,2.28-5.91,6.85-8.73,9.81-2.82,2.96-6.45,6.04-8.33,7.79-2.02,1.61-1.61,1.34-3.36,2.42l-6.99,4.03-4.03,2.02-6.72,2.15c-2.42.67-3.9,1.21-7.66,2.01l-14.91,2.82c-3.63.81-4.3,1.07-7.12,2.01-2.96,1.07-6.22,2.46-9.81,4.16-3.22,1.43-6.81,3.45-10.75,6.04-13.6,9.14-24.61,21.63-31.97,36.27-2.01,3.76-2.96,6.58-3.9,9.27-1.07,2.69-1.61,4.03-2.28,6.58-.67,2.69-1.48,5.24-2.15,9.27-.54,4.16-1.48,11.15-1.75,15.45-.27,4.16-.27,6.04,0,10.21l1.88,14.91c.67,3.76,1.48,5.91,2.15,8.19l1.88,5.64c.99,2.6,2.42,5.78,4.3,9.54,2.15,3.76,5.37,9.13,8.06,13.03,10.08,14.51,23.81,26.36,40.17,33.05,22.55,9.22,43.32,10.46,61.89,10.46,3.25,0,6.49-.09,9.71-.23,3.07-.42,2.74-6.09,10.48-9.42,5.26-2.26,13.7-2.42,14.91-2.55,1.07-.13-5.24-4.57-7.79-6.45-2.55-1.88-4.97-4.3-6.72-5.91-1.75-1.75-2.42-2.28-3.9-4.16-1.61-1.88-3.9-5.51-5.24-7.12-1.34-1.61-1.48-2.28-2.55-2.55-.98-.27-2.37-.09-4.16.54-1.88.4-5.37,1.21-7.12,1.88-1.75.67-2.02.81-3.09,2.01-1.07,1.07-2.69,3.76-3.63,4.7-.81.94-.67.67-1.48.94-.81.27-2.42.67-3.63.54-1.34-.18-2.55-.58-3.63-1.21-.81-.81-1.48-2.42-1.88-3.22-.4-.94-.54-1.21-.54-2.15l.4-3.49c.18-1.52.72-3.27,1.61-5.24.94-1.75,1.88-3.49,4.16-5.64,2.15-2.02,7.52-5.51,9.13-6.85,1.61-1.34,1.21-2.15.54-3.63-2.69-5.16-6.33-9.77-10.75-13.57-2.42-1.88-5.51-3.22-7.79-4.3-2.15-1.21-2.01-1.21-5.78-2.42-3.76-1.07-13.16-3.63-16.79-4.84-3.63-1.07-2.82-.94-5.1-2.01l-8.19-4.3c-1.75-.94-1.34-.54-2.69-1.61l-5.51-4.57c-2.18-1.93-3.77-4.45-4.57-7.25-.45-1.52-.45-2.87,0-4.03.4-1.34,0-1.21,2.01-3.63,2.15-2.42,7.25-7.93,10.21-10.75,3.09-2.82,4.97-4.03,7.79-6.04,5.43-3.82,11.09-7.32,16.93-10.48,2.42-1.21,5.37-2.02,7.12-2.69,1.61-.81,2.64-1.43,3.09-1.88.54-.4.67.54.4-.94-.4-1.48-1.88-4.97-2.28-7.66-.4-2.82-.4-6.18,0-8.73.36-2.42,1.03-4.66,2.01-6.72,1.16-2.24,2.46-4.3,3.9-6.18,1.34-1.7,2.91-3.04,4.7-4.03,1.75-1.07,4.3-2.01,6.18-2.42,1.61-.45,3.13-.58,4.57-.4l4.57.27c1.34.18,2.55.49,3.63.94,1.07.54,2.55,1.48,3.09,2.01.4.67.67.13-.27,1.61-1.07,1.48-4.16,4.57-5.78,7.12-1.61,2.69-2.96,5.6-4.03,8.73-.94,3.22-1.88,8.73-2.01,10.75-.27,2.15,4.7,1.75,5.1.54.54-1.07.27-2.55,1.07-5.1.81-2.42,2.42-7.25,3.9-9.81,1.34-2.55,2.82-3.76,4.57-5.51,1.79-1.88,3.67-3.45,5.64-4.7,1.88-1.21,3.22-1.75,5.64-2.28,2.42-.4,6.18-.67,8.73-.67,2.42,0,3.9.13,6.18.94,2.15.81,5.24,2.69,7.12,4.03,1.79,1.52,3.22,3.09,4.3,4.7,1.21,1.61,1.88,2.96,2.55,5.1.67,2.15,1.34,5.1,1.34,7.66,0,2.69-.54,6.04-1.21,8.19-.63,2.24-1.43,4.03-2.42,5.37-.67,1.34-.94,1.61-2.15,2.82-1.21,1.34-3.22,3.22-4.97,4.57-1.75,1.21-4.43,2.28-5.51,3.22-.94.81-.4,2.51.4,2.42.94.13,1.75.4,4.57-.13,2.82-.4,8.33-2.15,12.22-2.82,11.99-2.63,24.41-2.63,36.4,0,3.22.54,4.16,1.07,6.31,1.75l6.04,2.28c2.42,1.07,5.51,2.42,8.19,4.16,8.15,5.36,15.14,12.3,20.55,20.42,1.61,2.42,2.87,4.66,3.76,6.72,1.85,4.51,3.42,9.13,4.7,13.84.67,2.82,1.21,4.43,1.61,8.19.4,3.76.4,11.82.54,14.37.27,2.55-.13,2.28,1.75,1.34,1.75-.94,20.09-7.74,30.63-31.57,9.16-20.71,1.9-47.26-2.28-55.34-1.75-3.22-4.16-6.99-6.31-9.81-4.44-5.14-9.6-9.62-15.31-13.3-2.96-1.7-5.55-3.09-7.79-4.16-4.33-1.6-8.77-2.86-13.3-3.76l-6.18-.4h-14.37c-2.69-.27-3.1-3.18-.54-4.3,3.64-1.58,9.88-2.63,18.54-2.42,8.78.21,14.91,1.88,17.33,2.55,13.98,3.94,30.19,16.65,38.55,30.76,6.61,11.16,9.81,29.96,9.81,29.96.37,1.75,2.18,1.66,3.08.35ZM232.78,345.3c-3.91,0-7.08,3.17-7.08,7.08s3.17,7.08,7.08,7.08,7.08-3.17,7.08-7.08-3.17-7.08-7.08-7.08Z"/></svg>';
15096
15385
  }
15097
15386
  });
15098
15387
 
@@ -15805,7 +16094,7 @@ var init_remoteCodexExecutionContext = __esm({
15805
16094
  "SHELL",
15806
16095
  "TMPDIR"
15807
16096
  ]);
15808
- ignoredEnvOverrideKeys = /* @__PURE__ */ new Set(["PATH"]);
16097
+ ignoredEnvOverrideKeys = /* @__PURE__ */ new Set(["PATH", "GIT_PAGER"]);
15809
16098
  deniedEnvKeys = /* @__PURE__ */ new Set([
15810
16099
  "BASH_ENV",
15811
16100
  "ENV",
@@ -18430,7 +18719,7 @@ var linzumiCliVersion, linzumiCliVersionText;
18430
18719
  var init_version = __esm({
18431
18720
  "src/version.ts"() {
18432
18721
  "use strict";
18433
- linzumiCliVersion = "0.0.86-beta";
18722
+ linzumiCliVersion = "0.0.88-beta";
18434
18723
  linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
18435
18724
  }
18436
18725
  });
@@ -20819,7 +21108,7 @@ function createLinzumiMcpApiClient(options) {
20819
21108
  const baseUrl = kandanHttpBaseUrl(options.kandanUrl);
20820
21109
  const apiPrefix = options.authMode === "personal-agent-delegation" ? "/api/v2/personal-agent-mcp" : "/api/v2/local-runner-mcp";
20821
21110
  const operatingMode = options.operatingMode ?? "text";
20822
- const request = async (method, path2, params) => {
21111
+ const request = async (method, path2, params, requestOptions = {}) => {
20823
21112
  const url = new URL(path2, baseUrl);
20824
21113
  const paramsWithMode = {
20825
21114
  ...params,
@@ -20827,7 +21116,11 @@ function createLinzumiMcpApiClient(options) {
20827
21116
  };
20828
21117
  const requestInit = {
20829
21118
  method,
20830
- headers: { authorization: `Bearer ${options.accessToken}` }
21119
+ headers: { authorization: `Bearer ${options.accessToken}` },
21120
+ // An aborted signal cancels the in-flight fetch so a caller that has
21121
+ // already given up (e.g. a timed-out goal seed) cannot land a LATE POST
21122
+ // and clobber state written after it gave up (#1864 finding 2).
21123
+ ...requestOptions.signal === void 0 ? {} : { signal: requestOptions.signal }
20831
21124
  };
20832
21125
  if (method === "GET") {
20833
21126
  for (const [key, value] of Object.entries(paramsWithMode)) {
@@ -20860,9 +21153,11 @@ function createLinzumiMcpApiClient(options) {
20860
21153
  getMessage: (params) => request("GET", `${apiPrefix}/message`, params),
20861
21154
  getThread: (params) => request("GET", `${apiPrefix}/thread`, params),
20862
21155
  getChannel: (params) => request("GET", `${apiPrefix}/channel`, params),
21156
+ searchWorkspace: (params) => request("GET", `${apiPrefix}/search`, params),
21157
+ askWorkspace: (params) => request("POST", `${apiPrefix}/ask`, params),
20863
21158
  getCodingJobMetadata: (params) => request("GET", `${apiPrefix}/coding-job-metadata`, params),
20864
21159
  renameCodingJob: (params) => request("POST", `${apiPrefix}/coding-job-title`, params),
20865
- upsertCodingJobPlan: (params) => request("POST", `${apiPrefix}/coding-job-plan`, params),
21160
+ upsertCodingJobPlan: (params, requestOptions) => request("POST", `${apiPrefix}/coding-job-plan`, params, requestOptions),
20866
21161
  replaceCodingJobPlanSteps: (params) => request("POST", `${apiPrefix}/coding-job-plan/steps`, params),
20867
21162
  updateCodingJobPlanStep: (params) => request("POST", `${apiPrefix}/coding-job-plan/step`, params),
20868
21163
  linkCodingJobPullRequest: (params) => request("POST", `${apiPrefix}/coding-job-primary-pr`, params),
@@ -25649,29 +25944,18 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
25649
25944
  approvalPolicy: stringValue(control.approvalPolicy) ?? void 0
25650
25945
  });
25651
25946
  if (applied.approval_policy === "never") {
25652
- const pendingForSession = [
25653
- ...pendingClaudeCodeApprovals.entries()
25654
- ].filter(
25655
- ([, pending]) => pending.sessionId === void 0 || pending.sessionId === control.threadId
25656
- );
25657
- for (const [requestId, pending] of pendingForSession) {
25658
- pending.resolve({ type: "allow" });
25659
- await publishClaudeCodeMessageState(kandan, topic, {
25660
- workspace: pending.workspace,
25661
- channel: pending.channel,
25662
- threadId: pending.threadId,
25663
- sourceSeq: pending.sourceSeq,
25664
- status: "processing",
25665
- reason: "Claude Code tool approved",
25666
- sessionId: pending.sessionId ?? control.threadId
25667
- });
25668
- log2("claude_code.approval_request_auto_approved", {
25669
- request_id: requestId,
25670
- source_seq: pending.sourceSeq,
25671
- session_id: pending.sessionId ?? control.threadId,
25672
- reason: "full_access_enabled"
25673
- });
25674
- }
25947
+ await settleClaudeCodeApprovalsForSession({
25948
+ kandan,
25949
+ topic,
25950
+ pendingClaudeCodeApprovals,
25951
+ // activeClaudeCodeSessions is keyed by the SDK session id, so
25952
+ // control.threadId here is the SDK session id, NOT the linzumi thread
25953
+ // id. The drain matches pending approvals by their linzumi threadId,
25954
+ // so pass the session's stored linzumi threadId (Codex PR #1846 P1).
25955
+ sessionThreadId: claudeSession.threadId,
25956
+ log: log2,
25957
+ reason: "full_access_enabled"
25958
+ });
25675
25959
  }
25676
25960
  return {
25677
25961
  instanceId,
@@ -25800,12 +26084,18 @@ function truncateFailureDetail(message) {
25800
26084
  }
25801
26085
  function commanderDeveloperInstructions(args) {
25802
26086
  const agentLabel = args.agentLabel ?? "Codex";
25803
- const planStepsInstruction = args.planTool === "todo-write" ? `For coding-job threads, start by setting the current goal with
25804
- linzumi_upsert_coding_job_plan, then record the plan steps with the TodoWrite
25805
- tool. Linzumi mirrors your TodoWrite list into the coding-job plan steps
25806
- automatically, one TodoWrite item per plan step.` : `For coding-job threads, start by setting the current goal with
25807
- linzumi_upsert_coding_job_plan, then replace the plan steps with
25808
- linzumi_replace_coding_job_plan_steps.`;
26087
+ const planOpener = `Before you begin ANY work - and again whenever the job
26088
+ changes - call linzumi_upsert_coding_job_plan to set or refresh the current
26089
+ goal and the high-level plan. Always record the plan up front, before starting
26090
+ work, so the user can see a high-level view of what you are doing at all times
26091
+ and trust your work. Keep it accurate as you go: whenever you discover steps to
26092
+ add, steps that are no longer needed, or the nature of the job changes, update
26093
+ it with linzumi_upsert_coding_job_plan.`;
26094
+ const planStepsInstruction = args.planTool === "todo-write" ? `${planOpener}
26095
+ Record and maintain the plan STEPS with the TodoWrite tool. Linzumi mirrors
26096
+ your TodoWrite list into the coding-job plan steps automatically, one TodoWrite
26097
+ item per plan step.` : `${planOpener}
26098
+ Replace the plan STEPS with linzumi_replace_coding_job_plan_steps.`;
25809
26099
  const planUpdateInstruction = args.planTool === "todo-write" ? `As work proceeds, update each step's status with TodoWrite. Do not call
25810
26100
  linzumi_replace_coding_job_plan_steps or linzumi_update_coding_job_plan_step
25811
26101
  directly; TodoWrite is the source of truth for the plan steps.` : `As work proceeds, update each step with linzumi_update_coding_job_plan_step.`;
@@ -26194,6 +26484,14 @@ async function startCodexProviderInstance(args) {
26194
26484
  }
26195
26485
  };
26196
26486
  }
26487
+ function claudeCodeTurnStallThresholdMs(env) {
26488
+ const raw = env.LINZUMI_CLAUDE_TURN_STALL_MS?.trim();
26489
+ if (raw === void 0 || raw === "") {
26490
+ return defaultClaudeCodeTurnStallThresholdMs;
26491
+ }
26492
+ const parsed = Number.parseInt(raw, 10);
26493
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultClaudeCodeTurnStallThresholdMs;
26494
+ }
26197
26495
  function createClaudeCodeInputQueue(input) {
26198
26496
  const pendingMessages = [
26199
26497
  claudeCodeUserInputMessage(input.content)
@@ -26521,6 +26819,7 @@ async function startClaudeCodeProviderInstance(args) {
26521
26819
  args.options,
26522
26820
  args.control
26523
26821
  );
26822
+ const initialPermissionMode = runtimeSettings.approvalPolicy === void 0 ? void 0 : claudePermissionModeForApprovalPolicy(runtimeSettings.approvalPolicy);
26524
26823
  if (args.resumeSessionId !== void 0) {
26525
26824
  try {
26526
26825
  await validateClaudeCodeResumeSession({
@@ -26557,18 +26856,54 @@ async function startClaudeCodeProviderInstance(args) {
26557
26856
  let startControlResponded = false;
26558
26857
  let activeSessionId;
26559
26858
  let sessionControls;
26859
+ let startupApprovalDrain;
26560
26860
  const planMirrorClient = createLinzumiMcpApiClient({
26561
26861
  kandanUrl: args.options.kandanUrl,
26562
26862
  accessToken: args.options.token,
26563
26863
  fetchImpl: args.options.fetch,
26564
26864
  operatingMode: "text"
26565
26865
  });
26866
+ const seededGoal = workDescription.trim().slice(0, 2e3);
26566
26867
  const planMirror = createClaudeCodePlanMirror({
26567
26868
  replacePlanSteps: planMirrorClient.replaceCodingJobPlanSteps,
26568
26869
  threadId,
26569
- goal: workDescription.trim().slice(0, 2e3),
26870
+ goal: seededGoal,
26570
26871
  log: args.log
26571
26872
  });
26873
+ const seedCodingJobGoalBeforeTurn = async () => {
26874
+ if (args.resumeSessionId !== void 0) {
26875
+ return;
26876
+ }
26877
+ const seedTimeoutMs = 5e3;
26878
+ const seedAbort = new AbortController();
26879
+ let timeoutHandle;
26880
+ const timedOut = new Promise((resolve12) => {
26881
+ timeoutHandle = setTimeout(() => {
26882
+ seedAbort.abort(new Error("claude goal seed timed out"));
26883
+ args.log("claude_code.goal_seed_timeout", {
26884
+ thread_id: threadId,
26885
+ timeout_ms: seedTimeoutMs
26886
+ });
26887
+ resolve12();
26888
+ }, seedTimeoutMs);
26889
+ });
26890
+ try {
26891
+ await Promise.race([
26892
+ seedClaudeCodingJobGoal({
26893
+ upsertCodingJobPlan: planMirrorClient.upsertCodingJobPlan,
26894
+ threadId,
26895
+ goal: seededGoal,
26896
+ log: args.log,
26897
+ signal: seedAbort.signal
26898
+ }),
26899
+ timedOut
26900
+ ]);
26901
+ } finally {
26902
+ if (timeoutHandle !== void 0) {
26903
+ clearTimeout(timeoutHandle);
26904
+ }
26905
+ }
26906
+ };
26572
26907
  const adapter = createClaudeCodeSessionPipeline({
26573
26908
  instanceId: args.instanceId,
26574
26909
  cwd: args.cwd,
@@ -26874,6 +27209,7 @@ async function startClaudeCodeProviderInstance(args) {
26874
27209
  });
26875
27210
  const runSession = async () => {
26876
27211
  let result2;
27212
+ await seedCodingJobGoalBeforeTurn();
26877
27213
  try {
26878
27214
  try {
26879
27215
  result2 = await startClaudeCodeSession({
@@ -26887,9 +27223,35 @@ async function startClaudeCodeProviderInstance(args) {
26887
27223
  linzumiContext
26888
27224
  }),
26889
27225
  model: claudeCodeModelForRuntimeModel(runtimeSettings.model),
27226
+ // Apply the user's initial access level at session START, mirroring
27227
+ // codex (which seeds approvalPolicy / sandbox via
27228
+ // codexThreadRuntimeOverrides at start). Without this every Claude
27229
+ // Code session started in the SDK's 'default' mode and the no-prompt
27230
+ // ("never" -> bypassPermissions) level only took effect once an
27231
+ // update_session_settings control arrived post-start - so WebFetch
27232
+ // (no built-in allowlist) still raised an approval card on turn 1.
27233
+ // The mode is derived from runtimeSettings.approvalPolicy, which the
27234
+ // server already gates so only "never" (paired with a full-access
27235
+ // sandbox) survives to map to bypassPermissions; lesser levels keep
27236
+ // their 'default' / 'acceptEdits' modes.
27237
+ ...initialPermissionMode === void 0 ? {} : { permissionMode: initialPermissionMode },
26890
27238
  resumeSessionId: args.resumeSessionId,
26891
27239
  abortController,
26892
27240
  streamingInput: inputQueue,
27241
+ // PROD-RESILIENCE turn-stall watchdog (real-SDK sessions only; mock
27242
+ // runners drive their own timing). A turn that stays in flight with
27243
+ // no SDK output past the threshold is a wedged SILENT subprocess: the
27244
+ // watchdog aborts it and fails the turn so the thread reconnects,
27245
+ // instead of sitting "processing" forever (the ~28-min-hang incident).
27246
+ // The in-flight predicate (currentSourceSeq() is defined only while a
27247
+ // turn is queued/active) keeps a keep-alive session idling between
27248
+ // turns from being a false positive.
27249
+ ...args.options.claudeCodeRunner === void 0 ? {
27250
+ turnStallThresholdMs: claudeCodeTurnStallThresholdMs(
27251
+ process.env
27252
+ ),
27253
+ turnStallIsActive: () => inputQueue.currentSourceSeq() !== void 0
27254
+ } : {},
26893
27255
  runner: args.options.claudeCodeRunner,
26894
27256
  canUseTool,
26895
27257
  ...mcpServers === void 0 ? {} : { mcpServers },
@@ -26904,6 +27266,22 @@ async function startClaudeCodeProviderInstance(args) {
26904
27266
  allowedTools: ["mcp__linzumi__*"],
26905
27267
  onSessionControls: (controls) => {
26906
27268
  sessionControls = controls;
27269
+ if (initialPermissionMode === "bypassPermissions") {
27270
+ startupApprovalDrain = settleClaudeCodeApprovalsForSession({
27271
+ kandan: args.kandan,
27272
+ topic: args.topic,
27273
+ pendingClaudeCodeApprovals: args.pendingClaudeCodeApprovals,
27274
+ sessionThreadId: threadId,
27275
+ log: args.log,
27276
+ reason: "full_access_started"
27277
+ }).catch((error) => {
27278
+ args.log("claude_code.startup_approval_drain_failed", {
27279
+ linzumi_thread_id: threadId,
27280
+ claude_session_id: activeSessionId ?? null,
27281
+ message: error instanceof Error ? error.message : String(error)
27282
+ });
27283
+ });
27284
+ }
26907
27285
  },
26908
27286
  onTurnCompleted: () => {
26909
27287
  inputQueue.completeTurn();
@@ -26996,6 +27374,9 @@ async function startClaudeCodeProviderInstance(args) {
26996
27374
  message: error instanceof Error ? error.message : String(error)
26997
27375
  });
26998
27376
  }
27377
+ if (startupApprovalDrain !== void 0) {
27378
+ await startupApprovalDrain;
27379
+ }
26999
27380
  void sessionWork.then(
27000
27381
  () => {
27001
27382
  args.log("claude_code.session_loop_ended", {
@@ -27031,6 +27412,35 @@ function claudePermissionModeForApprovalPolicy(approvalPolicy) {
27031
27412
  return void 0;
27032
27413
  }
27033
27414
  }
27415
+ function claudeCodeApprovalsForSessionThread(pendingClaudeCodeApprovals, sessionThreadId) {
27416
+ return [...pendingClaudeCodeApprovals.entries()].filter(
27417
+ ([, pending]) => pending.threadId === sessionThreadId
27418
+ );
27419
+ }
27420
+ async function settleClaudeCodeApprovalsForSession(args) {
27421
+ const pendingForSession = claudeCodeApprovalsForSessionThread(
27422
+ args.pendingClaudeCodeApprovals,
27423
+ args.sessionThreadId
27424
+ );
27425
+ for (const [requestId, pending] of pendingForSession) {
27426
+ pending.resolve({ type: "allow" });
27427
+ await publishClaudeCodeMessageState(args.kandan, args.topic, {
27428
+ workspace: pending.workspace,
27429
+ channel: pending.channel,
27430
+ threadId: pending.threadId,
27431
+ sourceSeq: pending.sourceSeq,
27432
+ status: "processing",
27433
+ reason: "Claude Code tool approved",
27434
+ sessionId: pending.sessionId ?? args.sessionThreadId
27435
+ });
27436
+ args.log("claude_code.approval_request_auto_approved", {
27437
+ request_id: requestId,
27438
+ source_seq: pending.sourceSeq,
27439
+ session_id: pending.sessionId ?? args.sessionThreadId,
27440
+ reason: args.reason
27441
+ });
27442
+ }
27443
+ }
27034
27444
  function claudeSteerInputText(input) {
27035
27445
  const items = arrayValue(input) ?? [];
27036
27446
  const text2 = items.flatMap((item) => {
@@ -29202,6 +29612,7 @@ var init_runner = __esm({
29202
29612
  init_engineChildReaper();
29203
29613
  init_claudeCodeLiveBashOutput();
29204
29614
  init_claudeCodeSession();
29615
+ init_claudeCodeTurnStallWatchdog();
29205
29616
  init_codexAppServer();
29206
29617
  init_codexProjectTrust();
29207
29618
  init_codexRuntimeOptions();
@@ -65955,6 +66366,60 @@ async function runMcpServer(args) {
65955
66366
  },
65956
66367
  async (params) => mcpJsonResult(await client.getChannel(params))
65957
66368
  );
66369
+ server.tool(
66370
+ "linzumi_search_workspace",
66371
+ "Search the Linzumi workspace with hybrid lexical+semantic retrieval, scoped to what the authorizing user can access. Returns scored content chunks with provenance (channel, thread, authored time) and a permalink to cite.",
66372
+ {
66373
+ workspace: external_exports2.string().optional().describe(
66374
+ "Workspace slug. Omit to use the authenticated token scope."
66375
+ ),
66376
+ query: external_exports2.string().min(1).describe("Natural-language or keyword query to retrieve against."),
66377
+ limit: external_exports2.number().int().min(1).max(25).optional().describe("Maximum results to return (default 10, server-capped)."),
66378
+ source_types: external_exports2.array(
66379
+ external_exports2.enum([
66380
+ "message",
66381
+ "coding_job",
66382
+ "github_pr",
66383
+ "github_pr_review",
66384
+ "github_check_run",
66385
+ "github_workflow_run",
66386
+ "github_diff",
66387
+ "dictation",
66388
+ "file"
66389
+ ])
66390
+ ).optional().describe("Restrict results to these corpora; omit for all."),
66391
+ authored_after: external_exports2.string().optional().describe(
66392
+ "ISO8601 instant; only content authored at or after this time."
66393
+ ),
66394
+ authored_before: external_exports2.string().optional().describe(
66395
+ "ISO8601 instant; only content authored at or before this time."
66396
+ ),
66397
+ repo: external_exports2.string().optional().describe(
66398
+ "Restrict GitHub-sourced results to one repository by full name (owner/name)."
66399
+ ),
66400
+ check_status: external_exports2.enum(["pending", "success", "failed"]).optional().describe(
66401
+ "Restrict check and workflow results to one conclusion bucket."
66402
+ )
66403
+ },
66404
+ async (params) => mcpJsonResult(await client.searchWorkspace(params))
66405
+ );
66406
+ server.tool(
66407
+ "linzumi_ask_workspace",
66408
+ 'Ask the Linzumi workspace a natural-language question and get a synthesized, citation-backed answer over what the authorizing user can access. Costs an LLM call on cache misses, so prefer linzumi_search_workspace for simple lookups. A "refused" status means there was not enough accessible evidence - treat it as "the workspace does not know", never retry verbatim.',
66409
+ {
66410
+ workspace: external_exports2.string().optional().describe(
66411
+ "Workspace slug. Omit to use the authenticated token scope."
66412
+ ),
66413
+ question: external_exports2.string().min(1).max(2e3).describe("Natural-language question to answer with citations."),
66414
+ channel: external_exports2.string().optional().describe(
66415
+ "Optional channel slug providing conversational context; never widens access."
66416
+ ),
66417
+ allow_cached: external_exports2.boolean().optional().describe(
66418
+ "Allow serving a cached synthesis when the evidence set is unchanged (default true). Set false to force fresh synthesis."
66419
+ )
66420
+ },
66421
+ async (params) => mcpJsonResult(await client.askWorkspace(params))
66422
+ );
65958
66423
  server.tool(
65959
66424
  "linzumi_get_coding_job_metadata",
65960
66425
  "Read the active coding job workflow metadata for a thread, including the agent-owned goal, ordered plan steps, step lock versions, and workflow status.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.86-beta",
3
+ "version": "0.0.88-beta",
4
4
  "description": "Linzumi CLI — point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {