@adhdev/daemon-core 1.0.22 → 1.0.23-rc.1

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.
@@ -76,17 +76,29 @@ declare function scopeText(input: CliStatusInput, scope: SpinnerSpec['scope'] |
76
76
  declare function spinnerMatches(spec: SpinnerSpec, input: CliStatusInput): boolean;
77
77
  declare function settledPromptMatches(spec: SettledPromptSpec, input: CliStatusInput): boolean;
78
78
  /**
79
- * APPROVAL-PICKER-MISROUTE (mission f1d25e11) defense-in-depth: an
79
+ * APPROVAL-PICKER-MISROUTE (mission f1d25e11 / fb2a7053) defense-in-depth: an
80
80
  * AskUserQuestion multi-choice picker is NOT an approval modal. Its option rows
81
81
  * ("❯ 1. label") can otherwise satisfy the approval button cue and get
82
82
  * mis-classified as `waiting_approval`, so the worker's question is surfaced to
83
83
  * the coordinator as a task_approval_needed (→ mesh_approve, which cannot answer
84
- * it). The picker carries a distinctive signature the approval FSM never does:
85
- * the claude TUI select footer ("Enter to select … Esc to cancel") together with
86
- * the freeform escape hatch ("Type something" / "Chat about this"). When both are
87
- * present the screen is a question picker surfaced separately as
88
- * waiting_choice so the approval matchers must yield. Mirrors the legacy
89
- * looksLikeSelectionPicker guard (cli-provider-instance.ts) ported to SDK-v1.
84
+ * it), with no promptId for mesh_answer_question.
85
+ *
86
+ * The picker's distinguishing signature is its footer: the claude TUI select
87
+ * hints "Enter to select" AND "Esc to cancel". A genuine tool-consent approval
88
+ * modal (Yes/No/Allow/Deny) NEVER renders that footer pair it uses the ordinary
89
+ * "? for shortcuts" prompt line instead — so the footer alone reliably separates
90
+ * the two even though BOTH draw numbered option rows.
91
+ *
92
+ * The original guard additionally required the freeform escape hatch ("Type
93
+ * something" / "Chat about this"). That made the guard collapse whenever the
94
+ * escape-hatch rows were absent or scrolled out of the captured frame: the
95
+ * picker then fell through to the approval matchers → waiting_approval. The
96
+ * escape hatch is therefore DOWNGRADED from a required signal to an optional
97
+ * supporting one: the guard now fires on the select footer plus at least one
98
+ * numbered option row (which every picker draws), and the escape hatch merely
99
+ * reinforces an already-matching footer. The numbered-row requirement keeps a
100
+ * bare "Enter to select" string (e.g. inside prose) from being mistaken for a
101
+ * picker.
90
102
  */
91
103
  export declare function isAskUserQuestionPickerSignature(text: string): boolean;
92
104
  declare function modalMatches(spec: ModalSpec, input: CliStatusInput): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "1.0.22",
3
+ "version": "1.0.23-rc.1",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "*",
51
- "@adhdev/session-host-core": "*",
50
+ "@adhdev/mesh-shared": "1.0.23-rc.1",
51
+ "@adhdev/session-host-core": "1.0.23-rc.1",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -17,6 +17,7 @@ import { registerMeshCoordinator } from '../../mesh/coordinator-registry.js';
17
17
  import { partitionSessionHostRecords } from '../../session-host/runtime-surface.js';
18
18
  import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from '../mesh-coordinator.js';
19
19
  import { normalizeMeshNodeId } from '@adhdev/mesh-shared';
20
+ import { delegatedWorkerAutoApproveSettings } from '../../repo-mesh-types.js';
20
21
  import {
21
22
  readProviderPriorityFromPolicy,
22
23
  resolveProviderTypeFromPriority,
@@ -429,7 +430,25 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
429
430
  dir: workspace,
430
431
  cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
431
432
  env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
432
- settings: { meshCoordinatorFor: meshId },
433
+ settings: {
434
+ meshCoordinatorFor: meshId,
435
+ // AUTOAPPROVE-COORD: the coordinator is a mesh session too, so it
436
+ // must inherit the workspace's declarative auto-approve MODE
437
+ // (.adhdev/mesh.json providerDefaults.autoApproveModes) exactly like
438
+ // a delegated worker does at dispatch. Reuse the already-loaded repo
439
+ // config so the launch args carry --permission-mode. NB: we deliberately
440
+ // do NOT stamp launchedByCoordinator here — that flag is a WORKER-only
441
+ // dangerous-mode downgrade signal (auto-approve-modes.ts); the coordinator
442
+ // is the owner, not a worker. The dangerous gate is already applied inside
443
+ // delegatedWorkerAutoApproveSettings via mesh/node policy.
444
+ ...delegatedWorkerAutoApproveSettings(
445
+ effectiveMesh?.policy,
446
+ coordinatorNode?.policy,
447
+ providerMeta,
448
+ repoMeshConfigLoad.config,
449
+ cliType,
450
+ ),
451
+ },
433
452
  ...(initialModel ? { initialModel } : {}),
434
453
  ...(initialThinkingLevel ? { initialThinkingLevel } : {}),
435
454
  });
@@ -650,7 +669,17 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
650
669
  cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
651
670
  env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
652
671
  settings: {
653
- meshCoordinatorFor: meshId
672
+ meshCoordinatorFor: meshId,
673
+ // AUTOAPPROVE-COORD: inherit the workspace declarative auto-approve MODE
674
+ // for the coordinator session (see the cli_command branch for the full
675
+ // rationale). No launchedByCoordinator stamp — the coordinator is the owner.
676
+ ...delegatedWorkerAutoApproveSettings(
677
+ effectiveMesh?.policy,
678
+ coordinatorNode?.policy,
679
+ providerMeta,
680
+ repoMeshConfigLoad.config,
681
+ cliType,
682
+ ),
654
683
  },
655
684
  ...(initialModel ? { initialModel } : {}),
656
685
  ...(initialThinkingLevel ? { initialThinkingLevel } : {}),
@@ -204,30 +204,50 @@ function extractButtonLabels(spec: ModalSpec, text: string): string[] {
204
204
  * an assistant enumeration lacks that pair and does not fire the cue.
205
205
  */
206
206
  function buttonBlockApprovalCue(spec: ModalSpec, text: string): boolean {
207
+ // Defense-in-depth (mission fb2a7053): even if some caller reaches here on a
208
+ // question picker, its "Enter to select"/"Esc to cancel" footer marks it as a
209
+ // choice, not an approval — never derive an approval cue from picker rows.
210
+ if (/Enter to select/i.test(text) && /Esc to cancel/i.test(text)) return false;
207
211
  const labels = extractButtonLabels(spec, text);
208
212
  if (labels.length < 2) return false;
209
213
  if (pickApprovalButton(labels).index < 0) return false;
210
214
  return hasNegativeApprovalOption(labels);
211
215
  }
212
216
 
217
+ // A claude-cli AskUserQuestion picker option row: an optional cursor / checkbox
218
+ // followed by a "N." number marker. The picker draws AT LEAST one such row.
219
+ const PICKER_OPTION_ROW = /^\s*(?:[❯›>]\s*)?(?:\[[ xX]\]|[☐☒◻◼]\s*)?\d+[.)]\s+\S/m;
220
+
213
221
  /**
214
- * APPROVAL-PICKER-MISROUTE (mission f1d25e11) defense-in-depth: an
222
+ * APPROVAL-PICKER-MISROUTE (mission f1d25e11 / fb2a7053) defense-in-depth: an
215
223
  * AskUserQuestion multi-choice picker is NOT an approval modal. Its option rows
216
224
  * ("❯ 1. label") can otherwise satisfy the approval button cue and get
217
225
  * mis-classified as `waiting_approval`, so the worker's question is surfaced to
218
226
  * the coordinator as a task_approval_needed (→ mesh_approve, which cannot answer
219
- * it). The picker carries a distinctive signature the approval FSM never does:
220
- * the claude TUI select footer ("Enter to select … Esc to cancel") together with
221
- * the freeform escape hatch ("Type something" / "Chat about this"). When both are
222
- * present the screen is a question picker surfaced separately as
223
- * waiting_choice so the approval matchers must yield. Mirrors the legacy
224
- * looksLikeSelectionPicker guard (cli-provider-instance.ts) ported to SDK-v1.
227
+ * it), with no promptId for mesh_answer_question.
228
+ *
229
+ * The picker's distinguishing signature is its footer: the claude TUI select
230
+ * hints "Enter to select" AND "Esc to cancel". A genuine tool-consent approval
231
+ * modal (Yes/No/Allow/Deny) NEVER renders that footer pair it uses the ordinary
232
+ * "? for shortcuts" prompt line instead — so the footer alone reliably separates
233
+ * the two even though BOTH draw numbered option rows.
234
+ *
235
+ * The original guard additionally required the freeform escape hatch ("Type
236
+ * something" / "Chat about this"). That made the guard collapse whenever the
237
+ * escape-hatch rows were absent or scrolled out of the captured frame: the
238
+ * picker then fell through to the approval matchers → waiting_approval. The
239
+ * escape hatch is therefore DOWNGRADED from a required signal to an optional
240
+ * supporting one: the guard now fires on the select footer plus at least one
241
+ * numbered option row (which every picker draws), and the escape hatch merely
242
+ * reinforces an already-matching footer. The numbered-row requirement keeps a
243
+ * bare "Enter to select" string (e.g. inside prose) from being mistaken for a
244
+ * picker.
225
245
  */
226
246
  export function isAskUserQuestionPickerSignature(text: string): boolean {
227
247
  if (!text) return false;
228
248
  const hasSelectFooter = /Enter to select/i.test(text) && /Esc to cancel/i.test(text);
229
249
  if (!hasSelectFooter) return false;
230
- return /Type something\.?|Chat about this/i.test(text);
250
+ return PICKER_OPTION_ROW.test(text);
231
251
  }
232
252
 
233
253
  function modalMatches(spec: ModalSpec, input: CliStatusInput): boolean {
@@ -78,20 +78,30 @@ function compile(re: string, flags?: string): RegExp {
78
78
  }
79
79
  }
80
80
 
81
+ // See detect-status.PICKER_OPTION_ROW — kept in lockstep so both layers gate on
82
+ // the identical picker signature.
83
+ const PICKER_OPTION_ROW = /^\s*(?:[❯›>]\s*)?(?:\[[ xX]\]|[☐☒◻◼]\s*)?\d+[.)]\s+\S/m;
84
+
81
85
  /**
82
- * APPROVAL-PICKER-MISROUTE (mission f1d25e11) defense-in-depth: mirror
86
+ * APPROVAL-PICKER-MISROUTE (mission f1d25e11 / fb2a7053) defense-in-depth: mirror
83
87
  * detect-status.isAskUserQuestionPickerSignature at the button-parse layer. A
84
88
  * multi-choice AskUserQuestion picker is not an approval modal; its numbered
85
89
  * option rows can otherwise be extracted as approval buttons and produce a
86
- * spurious approval modal. When the picker signature (claude TUI select footer +
87
- * "Type something"/"Chat about this" freeform escape hatch) is present, return
88
- * null early so the screen is surfaced as waiting_choice, never approval.
90
+ * spurious approval modal.
91
+ *
92
+ * The picker signature is the claude TUI select footer ("Enter to select" +
93
+ * "Esc to cancel") plus at least one numbered option row. A genuine approval
94
+ * modal never renders that footer pair, so it is safe even though approval modals
95
+ * also draw numbered rows. The freeform escape hatch ("Type something" / "Chat
96
+ * about this") used to be REQUIRED here too, which broke the guard whenever the
97
+ * hatch rows were absent or scrolled out of frame; it is now downgraded to an
98
+ * optional supporting signal (mirrors detect-status).
89
99
  */
90
100
  function isAskUserQuestionPickerSignature(text: string): boolean {
91
101
  if (!text) return false;
92
102
  const hasSelectFooter = /Enter to select/i.test(text) && /Esc to cancel/i.test(text);
93
103
  if (!hasSelectFooter) return false;
94
- return /Type something\.?|Chat about this/i.test(text);
104
+ return PICKER_OPTION_ROW.test(text);
95
105
  }
96
106
 
97
107
  function findQuestionLineIndex(
@@ -303,8 +303,15 @@ function readClaudeOptionDescription(lines: string[], optionLineIndex: number):
303
303
  }
304
304
 
305
305
  function parseClaudeHeaderlessInteractiveTuiQuestion(page: ClaudeInteractiveTuiPage, index: number): InteractiveQuestion | null {
306
+ // The claude TUI select footer ("Enter to select" + "Esc to cancel") is the
307
+ // picker's defining signature. The freeform escape hatch ("Type something" /
308
+ // "Chat about this") used to be REQUIRED here, but those option rows can be
309
+ // absent or scrolled out of the captured frame — in which case the picker
310
+ // failed to parse, activeInteractivePrompt stayed null, and detect-status
311
+ // then mis-classified the screen as waiting_approval (mission fb2a7053).
312
+ // Requiring only the footer plus (below) a real option block is sufficient;
313
+ // the escape hatch, when present, is still picked up as allowFreeform.
306
314
  if (!isClaudeTuiSelectFooter(page.screenText)) return null;
307
- if (!/Type something\.?|Chat about this/i.test(page.screenText)) return null;
308
315
 
309
316
  const lines = page.screenText.split(/\r?\n/);
310
317
  let footerIndex = -1;