@adhdev/daemon-core 0.9.82-rc.360 → 0.9.82-rc.361

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.
package/dist/index.js CHANGED
@@ -316,10 +316,10 @@ function readInjected(value) {
316
316
  }
317
317
  function getDaemonBuildInfo() {
318
318
  if (cached) return cached;
319
- const commit = readInjected(true ? "ea4a65e3d2be71c4aba09dbb000df08d2111759e" : void 0) ?? "unknown";
320
- const commitShort = readInjected(true ? "ea4a65e3" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
321
- const version = readInjected(true ? "0.9.82-rc.360" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
322
- const builtAt = readInjected(true ? "2026-06-23T08:23:05.522Z" : void 0);
319
+ const commit = readInjected(true ? "4c8bb331f13862335f134d8a1b97fa17eaca01e0" : void 0) ?? "unknown";
320
+ const commitShort = readInjected(true ? "4c8bb331" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
321
+ const version = readInjected(true ? "0.9.82-rc.361" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
322
+ const builtAt = readInjected(true ? "2026-06-23T11:56:37.654Z" : void 0);
323
323
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
324
324
  return cached;
325
325
  }
@@ -14164,7 +14164,7 @@ function forwardUnresolvedDelegateEvent(components, routing, event) {
14164
14164
  }
14165
14165
  function ackUnresolvedDelegateForwardByFingerprint(coordinatorDaemonId, eventName, payload) {
14166
14166
  const match = peekUnresolvedDelegateForwards().find(
14167
- (entry) => entry.coordinatorDaemonId === coordinatorDaemonId && readNonEmptyString2(entry.payload.event) === eventName && readNonEmptyString2(entry.payload.targetSessionId || entry.payload.sessionId || entry.payload.instanceId) === readNonEmptyString2(payload.targetSessionId || payload.sessionId || payload.instanceId) && readNonEmptyString2(entry.payload.workspace) === readNonEmptyString2(payload.workspace)
14167
+ (entry) => daemonIdsEquivalent(entry.coordinatorDaemonId, coordinatorDaemonId) && readNonEmptyString2(entry.payload.event) === eventName && readNonEmptyString2(entry.payload.targetSessionId || entry.payload.sessionId || entry.payload.instanceId) === readNonEmptyString2(payload.targetSessionId || payload.sessionId || payload.instanceId) && readNonEmptyString2(entry.payload.workspace) === readNonEmptyString2(payload.workspace)
14168
14168
  );
14169
14169
  if (match) ackUnresolvedDelegateForward(match.id);
14170
14170
  }
@@ -15825,1011 +15825,1601 @@ var init_external_sources = __esm({
15825
15825
  }
15826
15826
  });
15827
15827
 
15828
- // src/cli-adapters/pty-transport.ts
15829
- var pty_transport_exports = {};
15830
- __export(pty_transport_exports, {
15831
- NodePtyTransportFactory: () => NodePtyTransportFactory
15832
- });
15833
- function loadNodePty() {
15834
- if (cachedPty !== void 0) return cachedPty;
15835
- try {
15836
- cachedPty = require("node-pty");
15837
- (0, import_session_host_core2.ensureNodePtySpawnHelperPermissions)();
15838
- } catch {
15839
- cachedPty = null;
15840
- }
15841
- return cachedPty;
15828
+ // src/providers/spec/fsm-types.ts
15829
+ function isV4Spec(raw) {
15830
+ return !!raw && typeof raw === "object" && raw.$schema === "adhdev:cli/spec@4";
15842
15831
  }
15843
- var os13, cachedPty, NodePtyRuntimeTransport, NodePtyTransportFactory;
15844
- var init_pty_transport = __esm({
15845
- "src/cli-adapters/pty-transport.ts"() {
15846
- "use strict";
15847
- os13 = __toESM(require("os"));
15848
- init_spawn_env();
15849
- init_resolve_executable();
15850
- NodePtyRuntimeTransport = class {
15851
- constructor(handle) {
15852
- this.handle = handle;
15853
- }
15854
- ready = Promise.resolve();
15855
- terminalQueriesHandled = false;
15856
- get pid() {
15857
- return this.handle.pid;
15858
- }
15859
- write(data) {
15860
- this.handle.write(data);
15861
- }
15862
- resize(cols, rows) {
15863
- this.handle.resize(cols, rows);
15864
- }
15865
- kill() {
15866
- this.handle.kill();
15867
- }
15868
- getMetadata() {
15869
- return null;
15870
- }
15871
- onData(callback) {
15872
- this.handle.onData(callback);
15873
- }
15874
- onExit(callback) {
15875
- this.handle.onExit(callback);
15876
- }
15877
- };
15878
- NodePtyTransportFactory = class {
15879
- spawn(command, args, options) {
15880
- const pty = loadNodePty();
15881
- if (!pty) throw new Error("node-pty is not installed");
15882
- let cwd = options.cwd;
15883
- if (cwd) {
15884
- try {
15885
- const fs32 = require("fs");
15886
- const stat2 = fs32.statSync(cwd);
15887
- if (!stat2.isDirectory()) cwd = os13.homedir();
15888
- } catch {
15889
- cwd = os13.homedir();
15890
- }
15891
- }
15892
- const handle = pty.spawn(resolveWin32Executable(command), args, {
15893
- name: "xterm-256color",
15894
- cols: options.cols,
15895
- rows: options.rows,
15896
- cwd,
15897
- env: options.env
15898
- });
15899
- return new NodePtyRuntimeTransport(handle);
15900
- }
15901
- };
15902
- }
15903
- });
15904
-
15905
- // src/providers/sdk/v1/builders/cli/visible-region.ts
15906
- function compile(re, flags) {
15907
- try {
15908
- return new RegExp(re, flags ?? "");
15909
- } catch (e) {
15910
- throw new Error(`Invalid visible-region anchor regex /${re}/${flags ?? ""}: ${e.message}`);
15911
- }
15832
+ function initialState(spec) {
15833
+ return spec.states.find((s2) => s2.initial) ?? spec.states[0];
15912
15834
  }
15913
- function findAllMatches(re, text) {
15914
- const results = [];
15915
- const iter = new RegExp(re.source, re.flags.replace("g", "") + "g");
15916
- let m;
15917
- while ((m = iter.exec(text)) !== null) {
15918
- results.push(m);
15919
- if (m[0].length === 0) iter.lastIndex += 1;
15920
- }
15921
- return results;
15835
+ function stateById(spec, id) {
15836
+ return spec.states.find((s2) => s2.id === id);
15922
15837
  }
15923
- function applyVisibleRegion(spec, text) {
15924
- if (!text) return text;
15925
- switch (spec.scope) {
15926
- case "screen":
15927
- case "buffer":
15928
- return text;
15929
- case "tail": {
15930
- const limit = spec.tailChars ?? 4e3;
15931
- if (text.length <= limit) return text;
15932
- return text.slice(-limit);
15933
- }
15934
- case "between-anchors": {
15935
- const anchors = spec.anchors;
15936
- if (!anchors?.top && !anchors?.bottom) return text;
15937
- const selectLast = (spec.selectAnchor ?? "first") === "last";
15938
- let topEnd = 0;
15939
- if (anchors.top) {
15940
- const topRe = compile(anchors.top.pattern, anchors.top.flags);
15941
- const topMatches = findAllMatches(topRe, text);
15942
- if (topMatches.length === 0) {
15943
- return text;
15944
- }
15945
- const chosen = selectLast ? topMatches[topMatches.length - 1] : topMatches[0];
15946
- topEnd = chosen.index + chosen[0].length;
15947
- }
15948
- let bottomStart = text.length;
15949
- if (anchors.bottom) {
15950
- const bottomRe = compile(anchors.bottom.pattern, anchors.bottom.flags);
15951
- const searchFrom = topEnd;
15952
- const suffix = text.slice(searchFrom);
15953
- const bottomMatches = findAllMatches(bottomRe, suffix);
15954
- if (bottomMatches.length === 0) {
15955
- return text;
15956
- }
15957
- const chosen = selectLast ? bottomMatches[bottomMatches.length - 1] : bottomMatches[0];
15958
- bottomStart = searchFrom + chosen.index;
15959
- }
15960
- if (topEnd >= bottomStart) {
15961
- return text;
15962
- }
15963
- return text.slice(topEnd, bottomStart);
15964
- }
15965
- default:
15966
- return text;
15967
- }
15838
+ function outgoingTransitions(spec, stateId) {
15839
+ const matches = spec.transitions.filter((t) => {
15840
+ if (t.from === "*") return true;
15841
+ if (Array.isArray(t.from)) return t.from.includes(stateId);
15842
+ return t.from === stateId;
15843
+ });
15844
+ return matches.map((t, i) => ({ t, i })).sort((a, b) => (b.t.priority ?? 0) - (a.t.priority ?? 0) || a.i - b.i).map((x) => x.t);
15968
15845
  }
15969
- var init_visible_region = __esm({
15970
- "src/providers/sdk/v1/builders/cli/visible-region.ts"() {
15846
+ function statusForState(state) {
15847
+ if (state.status) return state.status;
15848
+ if (state.modal) return "approval";
15849
+ if (state.id === "busy" || state.id === "generating") return "generating";
15850
+ return "idle";
15851
+ }
15852
+ function modalKindForState(state) {
15853
+ if (state.modal_kind) return state.modal_kind;
15854
+ if (state.modal) return "approval";
15855
+ return null;
15856
+ }
15857
+ var init_fsm_types = __esm({
15858
+ "src/providers/spec/fsm-types.ts"() {
15971
15859
  "use strict";
15972
15860
  }
15973
15861
  });
15974
15862
 
15975
- // src/providers/sdk/v1/builders/cli/detect-status.ts
15976
- function compile2(re, flags) {
15863
+ // src/providers/spec/fsm-loader.ts
15864
+ var fsm_loader_exports = {};
15865
+ __export(fsm_loader_exports, {
15866
+ loadFsmSpec: () => loadFsmSpec,
15867
+ validateFsmSpec: () => validateFsmSpec
15868
+ });
15869
+ function loadFsmSpec(sourcePath) {
15870
+ let raw;
15977
15871
  try {
15978
- return new RegExp(re, flags ?? "");
15979
- } catch (e) {
15980
- throw new Error(`Invalid regex /${re}/${flags ?? ""}: ${e.message}`);
15872
+ raw = JSON.parse(fs10.readFileSync(sourcePath, "utf8"));
15873
+ } catch (err) {
15874
+ return { ok: false, errors: [`Failed to read/parse spec: ${err.message}`], sourcePath };
15981
15875
  }
15876
+ const errors = validateFsmSpec(raw);
15877
+ if (errors.length) return { ok: false, errors, sourcePath };
15878
+ return { ok: true, spec: raw, sourcePath };
15982
15879
  }
15983
- function takeTail(text, lines) {
15984
- if (!text) return "";
15985
- const split = text.split("\n");
15986
- if (split.length <= lines) return text;
15987
- return split.slice(-lines).join("\n");
15988
- }
15989
- function scopeText(input, scope, windowLines) {
15990
- const lines = windowLines && windowLines > 0 ? windowLines : 8;
15991
- switch (scope) {
15992
- case "whole-screen":
15993
- return input.screenText ?? "";
15994
- case "recent-buffer":
15995
- return input.tail ?? "";
15996
- case "last-n-lines":
15997
- case "live-frame-tail":
15998
- case void 0:
15999
- default:
16000
- return takeTail(input.screenText ?? "", lines);
15880
+ function validateFsmSpec(raw) {
15881
+ const errs = [];
15882
+ if (!isV4Spec(raw)) return ['$schema must be "adhdev:cli/spec@4"'];
15883
+ const spec = raw;
15884
+ if (!spec.id) errs.push("id is required");
15885
+ if (!spec.binary) errs.push("binary is required");
15886
+ if (!spec.send_message?.submit_key) errs.push("send_message.submit_key is required");
15887
+ if (spec.pre_launch_trust !== void 0) {
15888
+ const t = spec.pre_launch_trust;
15889
+ if (!t || typeof t !== "object" || Array.isArray(t)) {
15890
+ errs.push("pre_launch_trust must be an object");
15891
+ } else {
15892
+ if (typeof t.settings_path !== "string" || !t.settings_path) errs.push("pre_launch_trust.settings_path is required");
15893
+ if (typeof t.key !== "string" || !t.key) errs.push("pre_launch_trust.key is required");
15894
+ }
16001
15895
  }
16002
- }
16003
- function compileSpinnerMatchers(spec) {
16004
- return spec.patterns.map((p) => compile2(p.regex, p.flags ?? "i"));
16005
- }
16006
- function compileSettledPromptMatchers(spec) {
16007
- const prompt = compile2(spec.regex, spec.flags ?? "m");
16008
- const footers = (spec.withFooter ?? []).map((f) => {
16009
- if (f.kind === "regex") {
16010
- const re = compile2(f.pattern, f.flags ?? "i");
16011
- return { test: (s2) => re.test(s2) };
15896
+ if (spec.refocus_when_stalled_ms !== void 0) {
15897
+ if (typeof spec.refocus_when_stalled_ms !== "number" || !(spec.refocus_when_stalled_ms > 0)) {
15898
+ errs.push("refocus_when_stalled_ms must be a positive number");
15899
+ } else if (!Array.isArray(spec.send_on_spawn) || spec.send_on_spawn.length === 0) {
15900
+ errs.push("refocus_when_stalled_ms requires send_on_spawn (the wake sequence to re-inject)");
16012
15901
  }
16013
- const needle = f.pattern.toLowerCase();
16014
- return { test: (s2) => s2.toLowerCase().includes(needle) };
16015
- });
16016
- return { prompt, footers };
16017
- }
16018
- function extractButtonLabels(spec, text) {
16019
- if (!text) return [];
16020
- const flags = spec.buttonFlags && spec.buttonFlags.includes("m") ? spec.buttonFlags : `${spec.buttonFlags ?? ""}m`;
16021
- const buttonRe = compile2(spec.buttonPattern, flags);
16022
- const labelGroup = Number.isInteger(spec.buttonLabelGroup) && (spec.buttonLabelGroup ?? 0) > 0 ? spec.buttonLabelGroup : 1;
16023
- const out = [];
16024
- for (const line of text.split("\n")) {
16025
- buttonRe.lastIndex = 0;
16026
- const m = buttonRe.exec(line);
16027
- if (!m) continue;
16028
- const captured = m[labelGroup] ?? (labelGroup === 1 && m.length > 2 ? m[m.length - 1] : void 0);
16029
- if (captured && captured.trim()) out.push(captured.trim());
16030
15902
  }
16031
- return out;
16032
- }
16033
- function buttonBlockApprovalCue(spec, text) {
16034
- const labels = extractButtonLabels(spec, text);
16035
- if (labels.length < 2) return false;
16036
- if (pickApprovalButton(labels).index < 0) return false;
16037
- return hasNegativeApprovalOption(labels);
16038
- }
16039
- function modalMatches(spec, input) {
16040
- const text = input.screenText ?? "";
16041
- const question = compile2(spec.questionPattern, spec.questionFlags ?? "i");
16042
- if (question.test(text)) return true;
16043
- for (const variant of spec.questionVariants ?? []) {
16044
- const re = compile2(variant.regex, variant.flags ?? "i");
16045
- if (re.test(text)) return true;
15903
+ if (!Array.isArray(spec.states) || spec.states.length === 0) {
15904
+ errs.push("states[] must be a non-empty array");
15905
+ return errs;
16046
15906
  }
16047
- if (buttonBlockApprovalCue(spec, text)) return true;
16048
- return false;
16049
- }
16050
- function evaluateGroup(group, spec, input, compiled) {
16051
- switch (group) {
16052
- case "spinner": {
16053
- if (!spec.spinner || !compiled.spinner) return null;
16054
- const text = scopeText(input, spec.spinner.scope, spec.spinner.scopeWindowLines);
16055
- return compiled.spinner.some((re) => re.test(text)) ? "generating" : null;
16056
- }
16057
- case "modal": {
16058
- if (!spec.modal) return null;
16059
- return modalMatches(spec.modal, input) ? "waiting_approval" : null;
15907
+ if (!Array.isArray(spec.transitions)) {
15908
+ errs.push("transitions[] must be an array");
15909
+ return errs;
15910
+ }
15911
+ const ids = /* @__PURE__ */ new Set();
15912
+ let initialCount = 0;
15913
+ for (const [i, s2] of spec.states.entries()) {
15914
+ if (!s2.id) {
15915
+ errs.push(`states[${i}].id is required`);
15916
+ continue;
16060
15917
  }
16061
- case "settled-prompt": {
16062
- if (!spec.settledPrompt || !compiled.settled) return null;
16063
- const text = scopeText(input, spec.settledPrompt.scope, spec.settledPrompt.scopeWindowLines);
16064
- if (!compiled.settled.prompt.test(text)) return null;
16065
- if (compiled.settled.footers.length === 0) return "idle";
16066
- return compiled.settled.footers.every((f) => f.test(text)) ? "idle" : null;
15918
+ if (ids.has(s2.id)) errs.push(`states[${i}].id "${s2.id}" is duplicated`);
15919
+ ids.add(s2.id);
15920
+ if (!s2.label) errs.push(`states[${i}].label is required`);
15921
+ if (s2.initial) initialCount += 1;
15922
+ if (s2.status && !["idle", "generating", "approval"].includes(s2.status)) {
15923
+ errs.push(`states[${i}].status "${s2.status}" must be idle|generating|approval`);
16067
15924
  }
16068
- // Groups declared in the catalog but not yet implemented as builder steps
16069
- // return null so they are no-ops in dispatch — declaring them in `order`
16070
- // is forward-compatible. Phase 2 Week 8+ will wire them.
16071
- case "cue-ordering":
16072
- case "error-detection":
16073
- case "approval-stitching":
16074
- return null;
16075
- default:
16076
- return null;
16077
15925
  }
16078
- }
16079
- function buildDetectStatusFromTui(spec) {
16080
- const compiledSpinner = spec.spinner ? compileSpinnerMatchers(spec.spinner) : null;
16081
- const compiledSettled = spec.settledPrompt ? compileSettledPromptMatchers(spec.settledPrompt) : null;
16082
- const compiled = { spinner: compiledSpinner, settled: compiledSettled };
16083
- const order = spec.dispatchOrder?.order && spec.dispatchOrder.order.length > 0 ? spec.dispatchOrder.order : DEFAULT_ORDER;
16084
- return function detectStatus(input) {
16085
- const effectiveInput = spec.visibleRegion ? {
16086
- ...input,
16087
- screenText: applyVisibleRegion(spec.visibleRegion, input.screenText ?? ""),
16088
- tail: applyVisibleRegion(spec.visibleRegion, input.tail)
16089
- } : input;
16090
- for (const group of order) {
16091
- const verdict = evaluateGroup(group, spec, effectiveInput, compiled);
16092
- if (verdict !== null) return verdict;
15926
+ if (initialCount === 0) errs.push("exactly one state must have initial:true (none found)");
15927
+ if (initialCount > 1) errs.push(`exactly one state must have initial:true (${initialCount} found)`);
15928
+ const sectionIds = new Set(Object.keys(spec.sections ?? {}));
15929
+ for (const [i, t] of spec.transitions.entries()) {
15930
+ const froms = t.from === "*" ? [] : Array.isArray(t.from) ? t.from : [t.from];
15931
+ for (const f of froms) {
15932
+ if (!ids.has(f)) errs.push(`transitions[${i}].from references unknown state "${f}"`);
16093
15933
  }
16094
- return null;
16095
- };
16096
- }
16097
- var DEFAULT_ORDER;
16098
- var init_detect_status = __esm({
16099
- "src/providers/sdk/v1/builders/cli/detect-status.ts"() {
16100
- "use strict";
16101
- init_visible_region();
16102
- init_approval_utils();
16103
- DEFAULT_ORDER = ["spinner", "modal", "settled-prompt"];
15934
+ if (t.from !== "*" && froms.length === 0) errs.push(`transitions[${i}].from is required`);
15935
+ if (!t.to) errs.push(`transitions[${i}].to is required`);
15936
+ else if (!ids.has(t.to)) errs.push(`transitions[${i}].to references unknown state "${t.to}"`);
15937
+ if (t.when) errs.push(...validateCondition(t.when, sectionIds, `transitions[${i}].when`));
16104
15938
  }
16105
- });
16106
-
16107
- // src/providers/sdk/v1/builders/cli/parse-approval.ts
16108
- function compile3(re, flags) {
16109
- try {
16110
- return new RegExp(re, flags);
16111
- } catch (e) {
16112
- throw new Error(`Invalid regex /${re}/${flags ?? ""}: ${e.message}`);
15939
+ for (const [i, s2] of spec.states.entries()) {
15940
+ const sec = s2.extract?.title?.section;
15941
+ if (sec && !sectionIds.has(sec)) errs.push(`states[${i}].extract.title.section "${sec}" unknown`);
15942
+ const bsec = s2.extract?.buttons?.section;
15943
+ if (bsec && !sectionIds.has(bsec)) errs.push(`states[${i}].extract.buttons.section "${bsec}" unknown`);
16113
15944
  }
15945
+ return errs;
16114
15946
  }
16115
- function findQuestionLineIndex(spec, lines) {
16116
- const primary = compile3(spec.questionPattern, spec.questionFlags ?? "i");
16117
- for (let i = lines.length - 1; i >= 0; i -= 1) {
16118
- if (primary.test(lines[i])) return { index: i, matchedSource: "primary" };
16119
- }
16120
- for (const variant of spec.questionVariants ?? []) {
16121
- const re = compile3(variant.regex, variant.flags ?? "i");
16122
- for (let i = lines.length - 1; i >= 0; i -= 1) {
16123
- if (re.test(lines[i])) return { index: i, matchedSource: variant.label ?? "variant" };
16124
- }
15947
+ function validateCondition(c, sectionIds, path42) {
15948
+ const errs = [];
15949
+ const w = c;
15950
+ if ("all" in w) {
15951
+ w.all.forEach((x, i) => errs.push(...validateCondition(x, sectionIds, `${path42}.all[${i}]`)));
15952
+ return errs;
16125
15953
  }
16126
- return null;
16127
- }
16128
- function scopeLines(spec, lines, questionIndex) {
16129
- const scope = spec.scope ?? "between-last-two-separators";
16130
- if (scope === "whole-screen") {
16131
- return { start: 0, end: lines.length };
15954
+ if ("any" in w) {
15955
+ w.any.forEach((x, i) => errs.push(...validateCondition(x, sectionIds, `${path42}.any[${i}]`)));
15956
+ return errs;
16132
15957
  }
16133
- if (scope === "window-around-question") {
16134
- const window = spec.scopeWindowLines ?? 16;
16135
- return {
16136
- start: Math.max(0, questionIndex - 2),
16137
- end: Math.min(lines.length, questionIndex + window)
16138
- };
15958
+ if ("not" in w) {
15959
+ errs.push(...validateCondition(w.not, sectionIds, `${path42}.not`));
15960
+ return errs;
16139
15961
  }
16140
- let lastSep = -1;
16141
- let prevSep = -1;
16142
- for (let i = lines.length - 1; i >= 0; i -= 1) {
16143
- if (SEPARATOR_RE.test(lines[i].trim())) {
16144
- if (lastSep < 0) lastSep = i;
16145
- else if (prevSep < 0) {
16146
- prevSep = i;
16147
- break;
16148
- }
15962
+ if ("matches" in w) {
15963
+ if (w.section && !sectionIds.has(w.section)) errs.push(`${path42}.section "${w.section}" unknown`);
15964
+ try {
15965
+ new RegExp(w.matches, w.flags ?? "i");
15966
+ } catch (e) {
15967
+ errs.push(`${path42}.matches invalid regex: ${e.message}`);
16149
15968
  }
15969
+ return errs;
16150
15970
  }
16151
- if (lastSep >= 0 && prevSep >= 0) {
16152
- return { start: prevSep, end: lastSep + 1 };
16153
- }
16154
- return {
16155
- start: Math.max(0, questionIndex - 4),
16156
- end: Math.min(lines.length, questionIndex + 16)
16157
- };
16158
- }
16159
- function extractButtons(spec, lines, windowStart, windowEnd) {
16160
- const buttonRe = compile3(spec.buttonPattern, spec.buttonFlags ?? "m");
16161
- const out = [];
16162
- const labelGroup = Number.isInteger(spec.buttonLabelGroup) && (spec.buttonLabelGroup ?? 0) > 0 ? spec.buttonLabelGroup : 1;
16163
- let i = windowStart;
16164
- while (i < windowEnd) {
16165
- const line = lines[i];
16166
- const m = buttonRe.exec(line);
16167
- const captured = m?.[labelGroup] ?? (labelGroup === 1 && m && m.length > 2 ? m[m.length - 1] : void 0);
16168
- if (m && captured) {
16169
- let label = captured.trim();
16170
- if (spec.continuationLines) {
16171
- let j = i + 1;
16172
- while (j < windowEnd) {
16173
- const next = lines[j];
16174
- if (!next.trim()) break;
16175
- if (buttonRe.test(next)) break;
16176
- if (!/^\s+/.test(next)) break;
16177
- label += " " + next.trim();
16178
- j += 1;
16179
- }
16180
- i = j;
16181
- } else {
16182
- i += 1;
16183
- }
16184
- out.push(label);
16185
- } else {
16186
- i += 1;
16187
- }
15971
+ if ("cursor_above" in w && "changed" in w) return errs;
15972
+ if ("elapsed_ms" in w) {
15973
+ if (typeof w.elapsed_ms !== "number") errs.push(`${path42}.elapsed_ms must be a number`);
15974
+ return errs;
16188
15975
  }
16189
- return out;
16190
- }
16191
- function buildMessage(spec, lines, questionIndex, windowStart, windowEnd) {
16192
- const messageParts = [];
16193
- if (spec.contextHeader) {
16194
- const ctxRe = compile3(spec.contextHeader.regex, (spec.contextHeader.flags ?? "i") + "m");
16195
- const haystack = lines.slice(windowStart, windowEnd).join("\n");
16196
- const m = ctxRe.exec(haystack);
16197
- if (m) messageParts.push(m[1] ? m[1].trim() : m[0].trim());
15976
+ if ("stable_ms" in w) {
15977
+ if (typeof w.stable_ms !== "number") errs.push(`${path42}.stable_ms must be a number`);
15978
+ return errs;
16198
15979
  }
16199
- messageParts.push(lines[questionIndex].trim());
16200
- return messageParts.filter(Boolean).join(" \u2014 ");
15980
+ errs.push(`${path42} is not a recognized condition`);
15981
+ return errs;
16201
15982
  }
16202
- function extractInlineButtons(spec, lines, windowStart, windowEnd) {
16203
- if (!spec.inlineButtonPattern) return [];
16204
- const declaredFlags = spec.inlineButtonFlags ?? "gi";
16205
- const flags = declaredFlags.includes("g") ? declaredFlags : declaredFlags + "g";
16206
- const re = compile3(spec.inlineButtonPattern, flags);
16207
- const out = [];
16208
- for (let i = windowStart; i < windowEnd; i += 1) {
16209
- let m;
16210
- re.lastIndex = 0;
16211
- while ((m = re.exec(lines[i])) !== null) {
16212
- const label = (m[1] ?? m[0]).trim();
16213
- if (label && !out.includes(label)) out.push(label);
16214
- if (m.index === re.lastIndex) re.lastIndex += 1;
16215
- }
15983
+ var fs10;
15984
+ var init_fsm_loader = __esm({
15985
+ "src/providers/spec/fsm-loader.ts"() {
15986
+ "use strict";
15987
+ fs10 = __toESM(require("fs"));
15988
+ init_fsm_types();
16216
15989
  }
16217
- return out;
15990
+ });
15991
+
15992
+ // src/providers/spec/evaluator.ts
15993
+ var evaluator_exports = {};
15994
+ __export(evaluator_exports, {
15995
+ evaluateCondition: () => evaluateCondition,
15996
+ extractButtonsFromRule: () => extractButtonsFromRule,
15997
+ extractTitle: () => extractTitle,
15998
+ lastContiguousNumberedBlock: () => lastContiguousNumberedBlock,
15999
+ resolveSections: () => resolveSections,
16000
+ sectionText: () => sectionText
16001
+ });
16002
+ function resolveSize(size, total) {
16003
+ if (size === void 0) return 0;
16004
+ if (typeof size === "number") return Math.max(0, Math.min(total, size));
16005
+ const m = /^(\d+(?:\.\d+)?)%$/.exec(size);
16006
+ if (!m) return 0;
16007
+ const pct = Number(m[1]);
16008
+ return Math.max(0, Math.min(total, Math.round(total * pct / 100)));
16218
16009
  }
16219
- function buildParseApprovalFromTui(spec, visibleRegion) {
16220
- const minButtons = spec.minButtons ?? 2;
16221
- return function parseApproval(input) {
16222
- const rawText = input.screenText ?? input.buffer ?? "";
16223
- if (!rawText) return null;
16224
- const text = visibleRegion ? applyVisibleRegion(visibleRegion, rawText) : rawText;
16225
- const lines = text.split("\n");
16226
- const question = findQuestionLineIndex(spec, lines);
16227
- if (!question) return null;
16228
- const { start, end } = scopeLines(spec, lines, question.index);
16229
- if (question.index < start || question.index >= end) return null;
16230
- let buttons = extractButtons(spec, lines, question.index + 1, end);
16231
- if (buttons.length < minButtons && spec.inlineButtonPattern) {
16232
- buttons = extractInlineButtons(spec, lines, question.index, end);
16010
+ function resolveSections(sectionsObj, lines) {
16011
+ const total = lines.length;
16012
+ const anchored = /* @__PURE__ */ new Map();
16013
+ const sectionEntries = Object.entries(sectionsObj);
16014
+ for (const [id, sec] of sectionEntries) {
16015
+ let from = 0;
16016
+ let to = total;
16017
+ if (sec.anchor !== void 0) {
16018
+ try {
16019
+ const anchorPatterns = Array.isArray(sec.anchor) ? sec.anchor : [sec.anchor];
16020
+ const sharedCtx = Array.isArray(sec.anchor_context) ? null : sec.anchor_context ?? null;
16021
+ const ctxList = Array.isArray(sec.anchor_context) ? sec.anchor_context : anchorPatterns.map(() => sharedCtx);
16022
+ const candidates = anchorPatterns.map((pattern, k) => {
16023
+ const ctx = ctxList[k] ?? null;
16024
+ return {
16025
+ re: new RegExp(pattern, sec.anchor_flags ?? ""),
16026
+ prevRe: ctx?.prev !== void 0 ? new RegExp(ctx.prev, ctx.prev_flags ?? "") : null,
16027
+ nextRe: ctx?.next !== void 0 ? new RegExp(ctx.next, ctx.next_flags ?? "") : null
16028
+ };
16029
+ });
16030
+ const matchesCandidate = (c, i) => c.re.test(lines[i]) && (c.prevRe === null || i > 0 && c.prevRe.test(lines[i - 1])) && (c.nextRe === null || i < total - 1 && c.nextRe.test(lines[i + 1]));
16031
+ let idx = -1;
16032
+ for (const c of candidates) {
16033
+ let candIdx = -1;
16034
+ if (sec.anchor_last) {
16035
+ for (let i = total - 1; i >= 0; i--) {
16036
+ if (matchesCandidate(c, i)) {
16037
+ candIdx = i;
16038
+ break;
16039
+ }
16040
+ }
16041
+ } else {
16042
+ for (let i = 0; i < total; i++) {
16043
+ if (matchesCandidate(c, i)) {
16044
+ candIdx = i;
16045
+ break;
16046
+ }
16047
+ }
16048
+ }
16049
+ if (candIdx !== -1 && (idx === -1 || candIdx < idx)) idx = candIdx;
16050
+ }
16051
+ if (idx !== -1) {
16052
+ from = idx;
16053
+ to = total;
16054
+ if (sec.until_regex !== void 0) {
16055
+ try {
16056
+ const ure = new RegExp(sec.until_regex, sec.until_regex_flags ?? "");
16057
+ const end = lines.findIndex((l, i) => i > idx && ure.test(l));
16058
+ if (end !== -1) to = end;
16059
+ } catch {
16060
+ }
16061
+ } else if (sec.lines !== void 0) {
16062
+ to = Math.min(total, from + sec.lines);
16063
+ }
16064
+ }
16065
+ } catch {
16066
+ }
16067
+ } else if (sec.from_top !== void 0) {
16068
+ from = resolveSize(sec.from_top, total);
16069
+ to = total;
16070
+ } else if (sec.from_bottom !== void 0) {
16071
+ const sz = resolveSize(sec.from_bottom, total);
16072
+ from = total - sz;
16073
+ to = total;
16233
16074
  }
16234
- if (buttons.length < minButtons) return null;
16235
- const message = buildMessage(spec, lines, question.index, start, end);
16236
- return { message, buttons };
16237
- };
16238
- }
16239
- var SEPARATOR_RE;
16240
- var init_parse_approval = __esm({
16241
- "src/providers/sdk/v1/builders/cli/parse-approval.ts"() {
16242
- "use strict";
16243
- init_visible_region();
16244
- SEPARATOR_RE = /^(?:─|━|═|━){10,}\s*$/;
16075
+ anchored.set(id, { fromLine: from, toLine: to });
16245
16076
  }
16246
- });
16247
-
16248
- // src/providers/sdk/v1/builders/cli/parse-session.ts
16249
- function stripAnsi2(text) {
16250
- return String(text || "").replace(/\x1b\[(\d*)C/g, (_m, n) => " ".repeat(Math.max(1, Number(n) || 1))).replace(/\x1b\[\d*D/g, "").replace(ANSI_RE, "").replace(OSC_RE, "").replace(/\x1b[P^_X][\s\S]*?(?:\x07|\x1b\\)/g, "").replace(/\x1b(?:[@-Z\\-_])/g, "");
16077
+ const resolved = [];
16078
+ for (const [id, sec] of sectionEntries) {
16079
+ let { fromLine, toLine } = anchored.get(id);
16080
+ if (sec.until !== void 0) {
16081
+ if (sec.until.startsWith("^")) {
16082
+ try {
16083
+ const ure = new RegExp(sec.until);
16084
+ const end = lines.findIndex((l, i) => i > fromLine && ure.test(l));
16085
+ if (end !== -1) toLine = end;
16086
+ } catch {
16087
+ }
16088
+ } else {
16089
+ const target = anchored.get(sec.until);
16090
+ if (target) toLine = target.fromLine;
16091
+ }
16092
+ }
16093
+ if (toLine < fromLine) toLine = fromLine;
16094
+ const text = lines.slice(fromLine, toLine).join("\n");
16095
+ resolved.push({ id, fromLine, toLine, text });
16096
+ }
16097
+ return resolved;
16251
16098
  }
16252
- function splitLines(text) {
16253
- return stripAnsi2(text).replace(//g, "").split(/\r?\n/).map((l) => l.replace(/\s+$/, ""));
16099
+ function sectionText(sections, sectionId, fullScreen) {
16100
+ if (!sectionId) return fullScreen;
16101
+ const found = sections.find((s2) => s2.id === sectionId);
16102
+ return found ? found.text : "";
16254
16103
  }
16255
- function pickInputText(input, scope) {
16256
- if (!input) return "";
16257
- if (scope === "screen") return String(input.screenText || input.screen?.text || "");
16258
- if (scope === "tail") return String(input.tail || input.recentBuffer || input.buffer || "");
16259
- return String(input.buffer || input.rawBuffer || input.screenText || "");
16104
+ function isRegexCondition(c) {
16105
+ return "matches" in c;
16260
16106
  }
16261
- function stableHash(value) {
16262
- return crypto3.createHash("sha1").update(String(value || "")).digest("hex").slice(0, 12);
16107
+ function isChangedCondition(c) {
16108
+ return "cursor_above" in c && "changed" in c;
16263
16109
  }
16264
- function normalizeMessageIdentity(messages, status) {
16265
- const list = Array.isArray(messages) ? messages : [];
16266
- let turnIndex = -1;
16267
- return list.map((message, index) => {
16268
- const role = message?.role || "assistant";
16269
- const kind = message?.kind || "standard";
16270
- const content = typeof message?.content === "string" ? message.content : "";
16271
- if (role === "user" || turnIndex < 0) turnIndex += 1;
16272
- const seed = [role, kind, "", index, content].join("\n");
16273
- const providerUnitKey = `v2-pty:${role}:${kind}:${index}:${stableHash(seed)}`;
16274
- const bubbleId = `bubble:${providerUnitKey}`;
16275
- const turnKey = `turn:${turnIndex}`;
16276
- const isStreamingTail = status === "generating" && role === "assistant" && index === list.length - 1;
16277
- return {
16278
- ...message,
16279
- providerUnitKey,
16280
- bubbleId,
16281
- sequence: index,
16282
- _turnKey: turnKey,
16283
- bubbleState: isStreamingTail ? "streaming" : "final"
16284
- };
16285
- });
16110
+ function isAllCondition(c) {
16111
+ return "all" in c;
16286
16112
  }
16287
- function buildParseSessionFromTui(spec) {
16288
- if (!spec.transcriptPty) {
16289
- throw new Error("buildParseSessionFromTui: spec.transcriptPty is required");
16113
+ function isAnyCondition(c) {
16114
+ return "any" in c;
16115
+ }
16116
+ function evaluateCondition(cond, sections, fullScreen, cursor, prevLines, trace, stateId) {
16117
+ if (isAllCondition(cond)) {
16118
+ for (const child of cond.all) {
16119
+ if (!evaluateCondition(child, sections, fullScreen, cursor, prevLines, trace, stateId)) {
16120
+ return false;
16121
+ }
16122
+ }
16123
+ return true;
16290
16124
  }
16291
- const assistantRe = new RegExp(spec.transcriptPty.assistantPrefix.regex, spec.transcriptPty.assistantPrefix.flags || "");
16292
- const userRe = spec.transcriptPty.userPrefix ? new RegExp(spec.transcriptPty.userPrefix.regex, spec.transcriptPty.userPrefix.flags || "") : null;
16293
- const toolRe = spec.transcriptPty.toolPrefix ? new RegExp(spec.transcriptPty.toolPrefix.regex, spec.transcriptPty.toolPrefix.flags || "") : null;
16294
- const toolSkip = spec.transcriptPty.toolPrefix?.skip ?? false;
16295
- const chromeRes = (spec.transcriptPty.chromePatterns || []).map((p) => new RegExp(p.regex, p.flags || ""));
16296
- const requireIndentForContinuation = spec.transcriptPty.continuationLine?.indented ?? false;
16297
- const stripLeadingChrome = spec.transcriptPty.stripLeadingChrome ?? true;
16298
- const scope = spec.transcriptPty.scope ?? "buffer";
16299
- const detectStatus = spec.spinner || spec.settledPrompt || spec.modal || spec.dispatchOrder ? buildDetectStatusFromTui({
16300
- spinner: spec.spinner,
16301
- settledPrompt: spec.settledPrompt,
16302
- modal: spec.modal,
16303
- dispatchOrder: spec.dispatchOrder
16304
- }) : () => null;
16305
- const parseApproval = spec.modal ? buildParseApprovalFromTui(spec.modal) : () => null;
16306
- const sessionIdRe = spec.sessionIdExtraction ? new RegExp(
16307
- spec.sessionIdExtraction.regex,
16308
- spec.sessionIdExtraction.flags ?? "i"
16309
- ) : null;
16310
- const sessionIdScope = spec.sessionIdExtraction?.scope ?? "tail";
16311
- return function parseSession(input) {
16312
- const status = detectStatus(input) ?? "idle";
16313
- const modal = parseApproval(input);
16314
- const text = pickInputText(input, scope);
16315
- const lines = splitLines(text);
16316
- const messages = [];
16317
- let seenFirstRoleLine = !stripLeadingChrome;
16318
- for (const raw of lines) {
16319
- const line = raw;
16320
- if (line.trim() === "") {
16321
- continue;
16125
+ if (isAnyCondition(cond)) {
16126
+ for (const child of cond.any) {
16127
+ if (evaluateCondition(child, sections, fullScreen, cursor, prevLines, trace, stateId)) {
16128
+ return true;
16322
16129
  }
16323
- let isChrome = false;
16324
- for (const cre of chromeRes) {
16325
- if (cre.test(line)) {
16326
- isChrome = true;
16327
- break;
16328
- }
16130
+ }
16131
+ return false;
16132
+ }
16133
+ if (isChangedCondition(cond)) {
16134
+ if (!cursor || !prevLines || prevLines.length === 0) return false;
16135
+ const curLines = fullScreen.split("\n");
16136
+ const startRow = Math.max(0, cursor.row - cond.cursor_above);
16137
+ const endRow = cursor.row;
16138
+ const currentSlice = curLines.slice(startRow, endRow).join("\n");
16139
+ const prevSlice = prevLines.slice(startRow, endRow).join("\n");
16140
+ const didChange = currentSlice !== prevSlice;
16141
+ const result = cond.changed ? didChange : !didChange;
16142
+ const stableSuffix = cond.stable_ms != null ? ` stable_ms=${cond.stable_ms}` : "";
16143
+ trace.push({
16144
+ kind: result ? "state_match" : "state_skip",
16145
+ text: `state[${stateId}] changed cond cursor_above=${cond.cursor_above} rows[${startRow},${endRow}) changed=${didChange} expected=${cond.changed}${stableSuffix} result=${result}`
16146
+ });
16147
+ return result;
16148
+ }
16149
+ if (isRegexCondition(cond)) {
16150
+ const haystack = sectionText(sections, cond.section, fullScreen);
16151
+ let matched = false;
16152
+ try {
16153
+ const re = new RegExp(cond.matches, cond.flags ?? "i");
16154
+ matched = re.test(haystack);
16155
+ } catch {
16156
+ matched = false;
16157
+ }
16158
+ if (!matched) {
16159
+ trace.push({ kind: "state_skip", text: `state[${stateId}] regex cond ${cond.section ?? "*"}~/${cond.matches}/ no match` });
16160
+ return false;
16161
+ }
16162
+ if (cursor !== void 0) {
16163
+ if (cond.cursor_row_min !== void 0 && cursor.row < cond.cursor_row_min) {
16164
+ trace.push({ kind: "state_skip", text: `state[${stateId}] cursor row ${cursor.row} < cursor_row_min ${cond.cursor_row_min}` });
16165
+ return false;
16329
16166
  }
16330
- if (isChrome) continue;
16331
- const userMatch = userRe ? line.match(userRe) : null;
16332
- const toolMatch = toolRe ? line.match(toolRe) : null;
16333
- const assistMatch = line.match(assistantRe);
16334
- if (userMatch) {
16335
- seenFirstRoleLine = true;
16336
- const content = (userMatch[1] ?? userMatch[0]).trim();
16337
- if (content) messages.push({ role: "user", kind: "standard", content });
16338
- continue;
16167
+ if (cond.cursor_row_max !== void 0 && cursor.row > cond.cursor_row_max) {
16168
+ trace.push({ kind: "state_skip", text: `state[${stateId}] cursor row ${cursor.row} > cursor_row_max ${cond.cursor_row_max}` });
16169
+ return false;
16339
16170
  }
16340
- if (toolMatch) {
16341
- seenFirstRoleLine = true;
16342
- if (toolSkip) continue;
16343
- const content = (toolMatch[1] ?? toolMatch[0]).trim();
16344
- if (content) messages.push({ role: "assistant", kind: "tool", content });
16345
- continue;
16171
+ if (cond.cursor_col_min !== void 0 && cursor.col < cond.cursor_col_min) {
16172
+ trace.push({ kind: "state_skip", text: `state[${stateId}] cursor col ${cursor.col} < cursor_col_min ${cond.cursor_col_min}` });
16173
+ return false;
16346
16174
  }
16347
- if (assistMatch) {
16348
- seenFirstRoleLine = true;
16349
- const content = (assistMatch[1] ?? assistMatch[0]).trim();
16350
- if (content) messages.push({ role: "assistant", kind: "standard", content });
16351
- continue;
16175
+ if (cond.cursor_col_max !== void 0 && cursor.col > cond.cursor_col_max) {
16176
+ trace.push({ kind: "state_skip", text: `state[${stateId}] cursor col ${cursor.col} > cursor_col_max ${cond.cursor_col_max}` });
16177
+ return false;
16352
16178
  }
16353
- if (!seenFirstRoleLine) continue;
16354
- if (requireIndentForContinuation && !/^\s/.test(line)) continue;
16355
- const last = messages[messages.length - 1];
16356
- if (!last) continue;
16357
- const cont = line.replace(/^\s+/, "");
16358
- if (cont) last.content = last.content ? `${last.content}
16359
- ${cont}` : cont;
16360
16179
  }
16361
- const result = {
16362
- status,
16363
- messages,
16364
- activeModal: modal,
16365
- modal,
16366
- parsedStatus: status
16367
- };
16368
- if (sessionIdRe) {
16369
- const haystack = stripAnsi2(pickInputText(input, sessionIdScope));
16370
- const match = haystack.match(sessionIdRe);
16371
- const captured = match?.[1]?.trim();
16372
- if (captured) result.providerSessionId = captured;
16180
+ trace.push({ kind: "state_match", text: `state[${stateId}] regex cond ${cond.section ?? "*"}~/${cond.matches}/ matched${cursor !== void 0 ? ` cursor=(${cursor.row},${cursor.col})` : ""}` });
16181
+ return true;
16182
+ }
16183
+ return false;
16184
+ }
16185
+ function extractTitle(rule, sections, fullScreen) {
16186
+ const hay = sectionText(sections, rule.section, fullScreen);
16187
+ if (!hay) return null;
16188
+ if (rule.first_line) {
16189
+ const lines = hay.split("\n");
16190
+ for (const line of lines) {
16191
+ const stripped = line.trim();
16192
+ if (stripped && !/^[─╌═─\s]+$/.test(stripped)) {
16193
+ return stripped;
16194
+ }
16373
16195
  }
16374
- return result;
16375
- };
16196
+ return null;
16197
+ }
16198
+ if (rule.regex) {
16199
+ try {
16200
+ const re = new RegExp(rule.regex, rule.flags ?? "i");
16201
+ const m = re.exec(hay);
16202
+ if (m) return (m[1] ?? m[0]).trim();
16203
+ } catch {
16204
+ }
16205
+ }
16206
+ return null;
16376
16207
  }
16377
- var crypto3, ANSI_RE, OSC_RE;
16378
- var init_parse_session = __esm({
16379
- "src/providers/sdk/v1/builders/cli/parse-session.ts"() {
16208
+ function compilePattern(ref) {
16209
+ const flags = ref.flags ?? "gm";
16210
+ return new RegExp(ref.pattern, flags.includes("g") ? flags : flags + "g");
16211
+ }
16212
+ function compileLinePattern(ref) {
16213
+ const flags = (ref.flags ?? "m").replace(/g/g, "");
16214
+ return new RegExp(ref.pattern, flags);
16215
+ }
16216
+ function extractButtonsFromRule(rule, hay) {
16217
+ const keyTemplate = rule.key_for_index;
16218
+ const continuationLines = rule.continuation_lines ?? false;
16219
+ const buttons = [];
16220
+ if (continuationLines) {
16221
+ const re = compileLinePattern(rule);
16222
+ const lines = hay.split("\n");
16223
+ for (let i = 0; i < lines.length; i += 1) {
16224
+ const m = re.exec(lines[i]);
16225
+ if (!m) continue;
16226
+ const idx = Number(m[1]);
16227
+ let label = String(m[2] ?? "").trim();
16228
+ if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
16229
+ const current = hasCursorMarker(lines[i]);
16230
+ let j = i + 1;
16231
+ while (j < lines.length) {
16232
+ const next = lines[j];
16233
+ if (!next.trim()) break;
16234
+ if (re.test(next)) break;
16235
+ if (!/^\s+/.test(next)) break;
16236
+ label += " " + next.trim();
16237
+ j += 1;
16238
+ }
16239
+ const key = keyTemplate.replace(/\{index\}/g, String(idx));
16240
+ buttons.push({ index: idx, label, key, current });
16241
+ i = j - 1;
16242
+ }
16243
+ } else {
16244
+ const re = compilePattern(rule);
16245
+ let m;
16246
+ while ((m = re.exec(hay)) !== null) {
16247
+ const idx = Number(m[1]);
16248
+ const label = String(m[2] ?? "").trim();
16249
+ if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
16250
+ const key = keyTemplate.replace(/\{index\}/g, String(idx));
16251
+ buttons.push({ index: idx, label, key, current: hasCursorMarker(m[0]) });
16252
+ }
16253
+ }
16254
+ const block2 = lastContiguousNumberedBlock(buttons);
16255
+ block2.sort((a, b) => a.index - b.index);
16256
+ return block2;
16257
+ }
16258
+ function lastContiguousNumberedBlock(entries) {
16259
+ if (entries.length <= 1) return entries.slice();
16260
+ let start = entries.length - 1;
16261
+ for (let i = entries.length - 1; i > 0; i -= 1) {
16262
+ if (entries[i - 1].index === entries[i].index - 1) start = i - 1;
16263
+ else break;
16264
+ }
16265
+ return entries.slice(start);
16266
+ }
16267
+ function hasCursorMarker(text) {
16268
+ return /^\s*[❯›>]/.test(text);
16269
+ }
16270
+ var init_evaluator = __esm({
16271
+ "src/providers/spec/evaluator.ts"() {
16380
16272
  "use strict";
16381
- init_detect_status();
16382
- init_parse_approval();
16383
- crypto3 = __toESM(require("crypto"));
16384
- ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
16385
- OSC_RE = /\x1b\][^\x07\x1b\n]*(?:\x07|\x1b\\|(?=\n|$))/g;
16386
16273
  }
16387
16274
  });
16388
16275
 
16389
- // src/cli-adapters/cli-script-runner.ts
16390
- function summarizeInput(input) {
16391
- const screenText = typeof input?.screenText === "string" ? input.screenText : "";
16392
- const rawBuffer = typeof input?.rawBuffer === "string" ? input.rawBuffer : "";
16393
- const tail = typeof input?.tail === "string" ? input.tail : "";
16394
- return {
16395
- screenTextLen: screenText.length,
16396
- rawBufferLen: rawBuffer.length,
16397
- tailLen: tail.length,
16398
- isWaitingForResponse: typeof input?.isWaitingForResponse === "boolean" ? input.isWaitingForResponse : void 0,
16399
- screenTextHead: screenText ? screenText.slice(0, 200) : void 0
16400
- };
16276
+ // src/providers/spec/fsm-evaluator.ts
16277
+ var fsm_evaluator_exports = {};
16278
+ __export(fsm_evaluator_exports, {
16279
+ evaluateConditionPreview: () => evaluateConditionPreview,
16280
+ evaluateFsm: () => evaluateFsm
16281
+ });
16282
+ function regionKey(cursorAbove) {
16283
+ return cursorAbove && cursorAbove > 0 ? cursorAbove : WHOLE_SCREEN;
16401
16284
  }
16402
- function summarizeResult(result) {
16403
- try {
16404
- const json = JSON.stringify(result);
16405
- return json && json.length > 400 ? `${json.slice(0, 400)}\u2026[truncated ${json.length - 400}]` : json ?? "undefined";
16406
- } catch (e) {
16407
- return `<unserializable: ${e?.message || e}>`;
16408
- }
16285
+ function isRegex(c) {
16286
+ return "matches" in c;
16409
16287
  }
16410
- var TRACE_RING_CAPACITY, DEFAULT_SCRIPT_CALL_BUDGET_MS, BUDGET_WARN_THROTTLE_MS, CliScriptRunner;
16411
- var init_cli_script_runner = __esm({
16412
- "src/cli-adapters/cli-script-runner.ts"() {
16413
- "use strict";
16414
- init_logger();
16415
- init_provider_cli_shared();
16416
- init_detect_status();
16417
- init_parse_approval();
16418
- init_parse_session();
16419
- TRACE_RING_CAPACITY = 64;
16420
- DEFAULT_SCRIPT_CALL_BUDGET_MS = 50;
16421
- BUDGET_WARN_THROTTLE_MS = 3e4;
16422
- CliScriptRunner = class {
16423
- scripts = {};
16424
- scriptState = null;
16425
- _parseErrorMessage = null;
16426
- cliType;
16427
- sdk = {};
16428
- invocationTrace = [];
16429
- /** Per-invocation wall-clock budget (ms). Configurable via setScriptCallBudget. */
16430
- scriptCallBudgetMs = DEFAULT_SCRIPT_CALL_BUDGET_MS;
16431
- /** Last WARN emit time per scriptName, used to throttle repeated budget violations. */
16432
- lastBudgetWarnAt = /* @__PURE__ */ new Map();
16433
- constructor(cliType) {
16434
- this.cliType = cliType;
16288
+ function isChanged(c) {
16289
+ return "cursor_above" in c && "changed" in c;
16290
+ }
16291
+ function isElapsed(c) {
16292
+ return "elapsed_ms" in c;
16293
+ }
16294
+ function isStable(c) {
16295
+ return "stable_ms" in c;
16296
+ }
16297
+ function isAll(c) {
16298
+ return "all" in c;
16299
+ }
16300
+ function isAny(c) {
16301
+ return "any" in c;
16302
+ }
16303
+ function isNot(c) {
16304
+ return "not" in c;
16305
+ }
16306
+ function evalCond(cond, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId) {
16307
+ if (isAll(cond)) {
16308
+ const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
16309
+ const result = children.every((c) => c.result);
16310
+ const remainingMs = result ? 0 : Math.max(0, ...children.filter((c) => !c.result).map((c) => c.remainingMs ?? 0));
16311
+ return { kind: "all", result, detail: `all(${children.length})`, remainingMs, children };
16312
+ }
16313
+ if (isAny(cond)) {
16314
+ const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
16315
+ const result = children.some((c) => c.result);
16316
+ const pending = children.filter((c) => !c.result).map((c) => c.remainingMs ?? Infinity);
16317
+ const remainingMs = result ? 0 : pending.length ? Math.min(...pending) : 0;
16318
+ return { kind: "any", result, detail: `any(${children.length})`, remainingMs: Number.isFinite(remainingMs) ? remainingMs : 0, children };
16319
+ }
16320
+ if (isNot(cond)) {
16321
+ const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId);
16322
+ return { kind: "not", result: !child.result, detail: `not`, remainingMs: 0, children: [child] };
16323
+ }
16324
+ if (isElapsed(cond)) {
16325
+ const age = clock.now - clock.stateEnteredAt;
16326
+ const result = age >= cond.elapsed_ms;
16327
+ const remainingMs = result ? 0 : cond.elapsed_ms - age;
16328
+ return { kind: "elapsed", result, detail: `elapsed ${age}ms / ${cond.elapsed_ms}ms`, remainingMs };
16329
+ }
16330
+ if (isStable(cond)) {
16331
+ const key = regionKey(cond.cursor_above);
16332
+ const lastChanged = clock.regionLastChangedAt.get(key) ?? clock.stateEnteredAt;
16333
+ const stableFor = clock.now - lastChanged;
16334
+ const result = stableFor >= cond.stable_ms;
16335
+ const remainingMs = result ? 0 : cond.stable_ms - stableFor;
16336
+ const where = key === WHOLE_SCREEN ? "screen" : `cursor_above=${cond.cursor_above}`;
16337
+ return { kind: "stable", result, detail: `stable ${where} ${stableFor}ms / ${cond.stable_ms}ms`, remainingMs };
16338
+ }
16339
+ if (isRegex(cond) || isChanged(cond)) {
16340
+ const result = evaluateCondition(cond, sections, fullScreen, cursor, prevLines, legacyTrace, stateId);
16341
+ const kind = isRegex(cond) ? "regex" : "changed";
16342
+ const detail = isRegex(cond) ? `${cond.section ?? "*"}~/${cond.matches}/` : `cursor_above=${cond.cursor_above} changed=${cond.changed}`;
16343
+ let matchedText;
16344
+ if (result && isRegex(cond)) {
16345
+ try {
16346
+ const hay = sectionText(sections, cond.section, fullScreen);
16347
+ const re = new RegExp(cond.matches, cond.flags ?? "i");
16348
+ const m = re.exec(hay);
16349
+ if (m && m[0]) matchedText = m[0].replace(/\s+/g, " ").trim().slice(0, 160);
16350
+ } catch {
16435
16351
  }
16436
- /** Returns the most-recent script invocation traces (oldest → newest). */
16437
- getInvocationTrace() {
16438
- return this.invocationTrace.slice();
16439
- }
16440
- /** Clear the trace ring — used by tests and after PTY reset. */
16441
- clearInvocationTrace() {
16442
- this.invocationTrace = [];
16443
- }
16444
- /**
16445
- * Configure the wall-clock budget (ms) applied to every script invocation.
16446
- *
16447
- * Out-of-range or non-finite values are clamped to [1, 5000] and the
16448
- * default (50ms) is used as a fallback. The budget is enforced per-call,
16449
- * not aggregated it does not abort a runaway script (Node CJS cannot
16450
- * interrupt synchronous code without a worker thread). Instead, an
16451
- * exceeded budget flags the trace entry with `timedOut: true` and emits
16452
- * a throttled WARN naming the script and elapsed time so an operator
16453
- * can identify which provider is hanging the settle loop.
16454
- */
16455
- setScriptCallBudget(ms) {
16456
- if (typeof ms !== "number" || !Number.isFinite(ms)) {
16457
- this.scriptCallBudgetMs = DEFAULT_SCRIPT_CALL_BUDGET_MS;
16458
- return;
16459
- }
16460
- const clamped = Math.max(1, Math.min(5e3, Math.floor(ms)));
16461
- this.scriptCallBudgetMs = clamped;
16462
- }
16463
- /** Test/debug accessor — current effective budget in ms. */
16464
- getScriptCallBudgetMs() {
16465
- return this.scriptCallBudgetMs;
16466
- }
16467
- recordTrace(entry) {
16468
- this.invocationTrace.push(entry);
16469
- if (this.invocationTrace.length > TRACE_RING_CAPACITY) {
16470
- this.invocationTrace.splice(0, this.invocationTrace.length - TRACE_RING_CAPACITY);
16471
- }
16472
- }
16473
- // ─── Script lifecycle ─────────────────────────────
16474
- setScripts(scripts, providerTui) {
16475
- this.sdk = this.buildSdk(providerTui);
16476
- const tui = providerTui;
16477
- const enriched = { ...scripts };
16478
- if (typeof enriched.detectStatus !== "function" && this.sdk.declarativeDetectStatus) {
16479
- enriched.detectStatus = this.sdk.declarativeDetectStatus;
16480
- }
16481
- if (typeof enriched.parseApproval !== "function" && this.sdk.declarativeParseApproval) {
16482
- enriched.parseApproval = this.sdk.declarativeParseApproval;
16483
- }
16484
- if (typeof enriched.parseSession !== "function" && tui?.transcriptPty) {
16485
- try {
16486
- const synth = buildParseSessionFromTui({
16487
- spinner: tui.spinner,
16488
- settledPrompt: tui.settledPrompt,
16489
- modal: tui.modal,
16490
- dispatchOrder: tui.dispatchOrder,
16491
- transcriptPty: tui.transcriptPty
16492
- });
16493
- enriched.parseSession = ((input) => {
16494
- const out = synth(input);
16495
- return {
16496
- ...out,
16497
- messages: normalizeMessageIdentity(out.messages, out.status ?? "idle")
16498
- };
16499
- });
16500
- } catch (e) {
16501
- LOG.warn("CLI", `[${this.cliType}] buildParseSessionFromTui failed: ${e?.message || e}`);
16502
- }
16503
- }
16504
- this.scripts = enriched;
16505
- this._parseErrorMessage = null;
16506
- this.scriptState = typeof enriched.createState === "function" ? enriched.createState() ?? null : null;
16507
- }
16508
- buildSdk(providerTui) {
16509
- const tui = providerTui;
16510
- if (!tui) return {};
16511
- const sdk = {};
16512
- if (tui.spinner || tui.settledPrompt || tui.modal || tui.dispatchOrder) {
16513
- try {
16514
- sdk.declarativeDetectStatus = buildDetectStatusFromTui({
16515
- spinner: tui.spinner,
16516
- settledPrompt: tui.settledPrompt,
16517
- modal: tui.modal,
16518
- dispatchOrder: tui.dispatchOrder
16519
- });
16520
- } catch (e) {
16521
- LOG.warn("CLI", `[${this.cliType}] buildDetectStatusFromTui failed: ${e?.message || e}`);
16522
- }
16523
- }
16524
- if (tui.modal) {
16525
- try {
16526
- sdk.declarativeParseApproval = buildParseApprovalFromTui(tui.modal);
16527
- } catch (e) {
16528
- LOG.warn("CLI", `[${this.cliType}] buildParseApprovalFromTui failed: ${e?.message || e}`);
16529
- }
16530
- }
16531
- return sdk;
16532
- }
16533
- /** Reset per-session state — called when the PTY process exits. */
16534
- resetSessionState() {
16535
- this.scriptState = null;
16536
- this.invocationTrace = [];
16537
- this.lastBudgetWarnAt.clear();
16538
- }
16539
- // ─── Script access (for reflection and test patching) ────────────────────
16540
- /** Returns the live scripts object. Direct property assignment on this object
16541
- * patches individual scripts without replacing others (used in tests). */
16542
- get cliScripts() {
16543
- return this.scripts;
16352
+ }
16353
+ return matchedText ? { kind, result, detail, matchedText } : { kind, result, detail };
16354
+ }
16355
+ return { kind: "all", result: false, detail: "unknown condition" };
16356
+ }
16357
+ function fromLabel(t) {
16358
+ const from = Array.isArray(t.from) ? t.from.join("|") : t.from;
16359
+ return t.label ?? `${from}\u2192${t.to}`;
16360
+ }
16361
+ function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock) {
16362
+ const legacyTrace = [];
16363
+ const lines = screenText.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
16364
+ const cleanScreen = lines.join("\n");
16365
+ const sections = resolveSections(spec.sections ?? {}, lines);
16366
+ const outgoing = outgoingTransitions(spec, currentStateId);
16367
+ const transitions = [];
16368
+ let fired = null;
16369
+ for (const t of outgoing) {
16370
+ const holdMs = t.min_hold_ms ?? 0;
16371
+ const heldFor = clock.now - clock.stateEnteredAt;
16372
+ const holdSatisfied = heldFor >= holdMs;
16373
+ const holdRemainingMs = holdSatisfied ? 0 : holdMs - heldFor;
16374
+ let cond;
16375
+ let condResult = true;
16376
+ if (t.when) {
16377
+ cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}`);
16378
+ condResult = cond.result;
16379
+ }
16380
+ const fires = holdSatisfied && condResult;
16381
+ const te = {
16382
+ to: t.to,
16383
+ label: fromLabel(t),
16384
+ eligible: true,
16385
+ holdSatisfied,
16386
+ holdRemainingMs,
16387
+ condResult,
16388
+ cond,
16389
+ fires,
16390
+ priority: t.priority ?? 0
16391
+ };
16392
+ transitions.push(te);
16393
+ if (fires && !fired) fired = te;
16394
+ }
16395
+ if (fired && !stateById(spec, fired.to)) {
16396
+ legacyTrace.push({ kind: "state_skip", text: `transition target ${fired.to} not found` });
16397
+ fired = null;
16398
+ }
16399
+ return { sections, transitions, fired, trace: legacyTrace };
16400
+ }
16401
+ function evaluateConditionPreview(cond, sections, screenText, cursor) {
16402
+ const lines = screenText.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
16403
+ const cleanScreen = lines.join("\n");
16404
+ const resolved = resolveSections(sections ?? {}, lines);
16405
+ const clock = { now: 0, stateEnteredAt: 0, regionLastChangedAt: /* @__PURE__ */ new Map() };
16406
+ return evalCond(cond, resolved, cleanScreen, cursor, void 0, clock, [], "preview");
16407
+ }
16408
+ var WHOLE_SCREEN;
16409
+ var init_fsm_evaluator = __esm({
16410
+ "src/providers/spec/fsm-evaluator.ts"() {
16411
+ "use strict";
16412
+ init_evaluator();
16413
+ init_fsm_types();
16414
+ WHOLE_SCREEN = -1;
16415
+ }
16416
+ });
16417
+
16418
+ // src/cli-adapters/pty-transport.ts
16419
+ var pty_transport_exports = {};
16420
+ __export(pty_transport_exports, {
16421
+ NodePtyTransportFactory: () => NodePtyTransportFactory
16422
+ });
16423
+ function loadNodePty() {
16424
+ if (cachedPty !== void 0) return cachedPty;
16425
+ try {
16426
+ cachedPty = require("node-pty");
16427
+ (0, import_session_host_core2.ensureNodePtySpawnHelperPermissions)();
16428
+ } catch {
16429
+ cachedPty = null;
16430
+ }
16431
+ return cachedPty;
16432
+ }
16433
+ var os13, cachedPty, NodePtyRuntimeTransport, NodePtyTransportFactory;
16434
+ var init_pty_transport = __esm({
16435
+ "src/cli-adapters/pty-transport.ts"() {
16436
+ "use strict";
16437
+ os13 = __toESM(require("os"));
16438
+ init_spawn_env();
16439
+ init_resolve_executable();
16440
+ NodePtyRuntimeTransport = class {
16441
+ constructor(handle) {
16442
+ this.handle = handle;
16544
16443
  }
16545
- // ─── Capability checks ────────────────────────────
16546
- hasDetectStatus() {
16547
- return typeof this.scripts.detectStatus === "function";
16444
+ ready = Promise.resolve();
16445
+ terminalQueriesHandled = false;
16446
+ get pid() {
16447
+ return this.handle.pid;
16548
16448
  }
16549
- hasParseSession() {
16550
- return typeof this.scripts.parseSession === "function";
16449
+ write(data) {
16450
+ this.handle.write(data);
16551
16451
  }
16552
- getScriptNames() {
16553
- return listCliScriptNames(this.scripts);
16452
+ resize(cols, rows) {
16453
+ this.handle.resize(cols, rows);
16554
16454
  }
16555
- // ─── Error state ──────────────────────────────────
16556
- get parseErrorMessage() {
16557
- return this._parseErrorMessage;
16455
+ kill() {
16456
+ this.handle.kill();
16558
16457
  }
16559
- clearParseError() {
16560
- this._parseErrorMessage = null;
16458
+ getMetadata() {
16459
+ return null;
16561
16460
  }
16562
- // ─── Script invocation ────────────────────────────
16563
- detectStatus(input) {
16564
- if (!this.scripts.detectStatus) return null;
16565
- try {
16566
- return this.invoke("detectStatus", this.scripts.detectStatus, input);
16567
- } catch (e) {
16568
- LOG.warn("CLI", `[${this.cliType}] detectStatus error: ${e?.message || e}`);
16569
- return null;
16570
- }
16461
+ onData(callback) {
16462
+ this.handle.onData(callback);
16571
16463
  }
16572
- parseApproval(input) {
16573
- if (!this.scripts.parseApproval) return null;
16574
- try {
16575
- return this.invoke(
16576
- "parseApproval",
16577
- this.scripts.parseApproval,
16578
- input
16579
- );
16580
- } catch (e) {
16581
- LOG.warn("CLI", `[${this.cliType}] parseApproval error: ${e?.message || e}`);
16582
- return null;
16583
- }
16464
+ onExit(callback) {
16465
+ this.handle.onExit(callback);
16584
16466
  }
16585
- parseSession(input) {
16586
- if (!this.scripts.parseSession) {
16587
- this._parseErrorMessage = `${this.cliType} parseSession unavailable`;
16588
- return null;
16589
- }
16590
- try {
16591
- const result = this.invoke("parseSession", this.scripts.parseSession, input);
16592
- this._parseErrorMessage = null;
16593
- return result && typeof result === "object" ? result : null;
16594
- } catch (e) {
16595
- this._parseErrorMessage = e?.message || String(e);
16596
- LOG.warn("CLI", `[${this.cliType}] parseSession error: ${this._parseErrorMessage}`);
16597
- return null;
16467
+ };
16468
+ NodePtyTransportFactory = class {
16469
+ spawn(command, args, options) {
16470
+ const pty = loadNodePty();
16471
+ if (!pty) throw new Error("node-pty is not installed");
16472
+ let cwd = options.cwd;
16473
+ if (cwd) {
16474
+ try {
16475
+ const fs32 = require("fs");
16476
+ const stat2 = fs32.statSync(cwd);
16477
+ if (!stat2.isDirectory()) cwd = os13.homedir();
16478
+ } catch {
16479
+ cwd = os13.homedir();
16480
+ }
16598
16481
  }
16482
+ const handle = pty.spawn(resolveWin32Executable(command), args, {
16483
+ name: "xterm-256color",
16484
+ cols: options.cols,
16485
+ rows: options.rows,
16486
+ cwd,
16487
+ env: options.env
16488
+ });
16489
+ return new NodePtyRuntimeTransport(handle);
16599
16490
  }
16600
- /**
16601
- * Invoke an arbitrary named script (e.g. setModel, openModelPicker).
16602
- * Throws if the script is not available.
16603
- */
16604
- invokeByName(name, input) {
16605
- const fn = this.scripts[name];
16606
- if (typeof fn !== "function") {
16607
- throw new Error(`CLI script '${name}' not available`);
16491
+ };
16492
+ }
16493
+ });
16494
+
16495
+ // src/providers/sdk/v1/builders/cli/visible-region.ts
16496
+ function compile(re, flags) {
16497
+ try {
16498
+ return new RegExp(re, flags ?? "");
16499
+ } catch (e) {
16500
+ throw new Error(`Invalid visible-region anchor regex /${re}/${flags ?? ""}: ${e.message}`);
16501
+ }
16502
+ }
16503
+ function findAllMatches(re, text) {
16504
+ const results = [];
16505
+ const iter = new RegExp(re.source, re.flags.replace("g", "") + "g");
16506
+ let m;
16507
+ while ((m = iter.exec(text)) !== null) {
16508
+ results.push(m);
16509
+ if (m[0].length === 0) iter.lastIndex += 1;
16510
+ }
16511
+ return results;
16512
+ }
16513
+ function applyVisibleRegion(spec, text) {
16514
+ if (!text) return text;
16515
+ switch (spec.scope) {
16516
+ case "screen":
16517
+ case "buffer":
16518
+ return text;
16519
+ case "tail": {
16520
+ const limit = spec.tailChars ?? 4e3;
16521
+ if (text.length <= limit) return text;
16522
+ return text.slice(-limit);
16523
+ }
16524
+ case "between-anchors": {
16525
+ const anchors = spec.anchors;
16526
+ if (!anchors?.top && !anchors?.bottom) return text;
16527
+ const selectLast = (spec.selectAnchor ?? "first") === "last";
16528
+ let topEnd = 0;
16529
+ if (anchors.top) {
16530
+ const topRe = compile(anchors.top.pattern, anchors.top.flags);
16531
+ const topMatches = findAllMatches(topRe, text);
16532
+ if (topMatches.length === 0) {
16533
+ return text;
16608
16534
  }
16609
- return this.invoke(name, fn, input);
16535
+ const chosen = selectLast ? topMatches[topMatches.length - 1] : topMatches[0];
16536
+ topEnd = chosen.index + chosen[0].length;
16610
16537
  }
16611
- // ─── Internal ─────────────────────────────────────
16612
- invoke(scriptName, fn, input) {
16613
- const arity = fn.length;
16614
- const startedAt = Date.now();
16615
- const startedHr = typeof process !== "undefined" && typeof process.hrtime === "function" ? process.hrtime.bigint() : null;
16616
- let result;
16617
- try {
16618
- if (arity >= 3) {
16619
- result = fn(this.scriptState, input, this.sdk);
16620
- } else if (arity === 2) {
16621
- result = fn(this.scriptState, input);
16622
- } else {
16623
- result = fn(input);
16624
- }
16625
- const elapsedUs = startedHr ? Number((process.hrtime.bigint() - startedHr) / 1000n) : 0;
16626
- const timedOut = this.checkBudget(scriptName, elapsedUs);
16627
- this.recordTrace({
16628
- at: startedAt,
16629
- scriptName,
16630
- arity,
16631
- inputSummary: summarizeInput(input),
16632
- ok: true,
16633
- elapsedUs,
16634
- resultSummary: summarizeResult(result),
16635
- ...timedOut ? { timedOut: true } : {}
16636
- });
16637
- return result;
16638
- } catch (e) {
16639
- const elapsedUs = startedHr ? Number((process.hrtime.bigint() - startedHr) / 1000n) : 0;
16640
- const timedOut = this.checkBudget(scriptName, elapsedUs);
16641
- this.recordTrace({
16642
- at: startedAt,
16643
- scriptName,
16644
- arity,
16645
- inputSummary: summarizeInput(input),
16646
- ok: false,
16647
- elapsedUs,
16648
- error: e?.message ? String(e.message).slice(0, 400) : String(e).slice(0, 400),
16649
- ...timedOut ? { timedOut: true } : {}
16650
- });
16651
- throw e;
16538
+ let bottomStart = text.length;
16539
+ if (anchors.bottom) {
16540
+ const bottomRe = compile(anchors.bottom.pattern, anchors.bottom.flags);
16541
+ const searchFrom = topEnd;
16542
+ const suffix = text.slice(searchFrom);
16543
+ const bottomMatches = findAllMatches(bottomRe, suffix);
16544
+ if (bottomMatches.length === 0) {
16545
+ return text;
16652
16546
  }
16547
+ const chosen = selectLast ? bottomMatches[bottomMatches.length - 1] : bottomMatches[0];
16548
+ bottomStart = searchFrom + chosen.index;
16653
16549
  }
16654
- /**
16655
- * Returns true when `elapsedUs` exceeded the configured budget. On the
16656
- * first violation per script (or after the throttle window expires) we
16657
- * emit a single WARN so the operator learns which provider is slow.
16658
- *
16659
- * We deliberately throttle per-scriptName so a chronically-slow
16660
- * detectStatus doesn't spam the log on every PTY frame settle.
16661
- */
16662
- checkBudget(scriptName, elapsedUs) {
16663
- const budgetUs = this.scriptCallBudgetMs * 1e3;
16664
- if (elapsedUs <= budgetUs) return false;
16665
- const now = Date.now();
16666
- const last = this.lastBudgetWarnAt.get(scriptName) ?? 0;
16667
- if (now - last >= BUDGET_WARN_THROTTLE_MS) {
16668
- this.lastBudgetWarnAt.set(scriptName, now);
16669
- LOG.warn(
16670
- "CLI",
16671
- `[${this.cliType}] script ${scriptName} took ${elapsedUs}us, budget ${budgetUs}us`
16672
- );
16673
- }
16674
- return true;
16550
+ if (topEnd >= bottomStart) {
16551
+ return text;
16675
16552
  }
16676
- };
16553
+ return text.slice(topEnd, bottomStart);
16554
+ }
16555
+ default:
16556
+ return text;
16557
+ }
16558
+ }
16559
+ var init_visible_region = __esm({
16560
+ "src/providers/sdk/v1/builders/cli/visible-region.ts"() {
16561
+ "use strict";
16677
16562
  }
16678
16563
  });
16679
16564
 
16680
- // src/cli-adapters/provider-cli-parse.ts
16681
- function sliceFromOffset(text, start) {
16565
+ // src/providers/sdk/v1/builders/cli/detect-status.ts
16566
+ function compile2(re, flags) {
16567
+ try {
16568
+ return new RegExp(re, flags ?? "");
16569
+ } catch (e) {
16570
+ throw new Error(`Invalid regex /${re}/${flags ?? ""}: ${e.message}`);
16571
+ }
16572
+ }
16573
+ function takeTail(text, lines) {
16682
16574
  if (!text) return "";
16683
- if (!Number.isFinite(start) || start <= 0) return text;
16684
- if (start >= text.length) return "";
16685
- return text.slice(start);
16575
+ const split = text.split("\n");
16576
+ if (split.length <= lines) return text;
16577
+ return split.slice(-lines).join("\n");
16686
16578
  }
16687
- function normalizeCliParsedMessages(parsedMessages, _options) {
16688
- return Array.isArray(parsedMessages) ? parsedMessages : [];
16579
+ function scopeText(input, scope, windowLines) {
16580
+ const lines = windowLines && windowLines > 0 ? windowLines : 8;
16581
+ switch (scope) {
16582
+ case "whole-screen":
16583
+ return input.screenText ?? "";
16584
+ case "recent-buffer":
16585
+ return input.tail ?? "";
16586
+ case "last-n-lines":
16587
+ case "live-frame-tail":
16588
+ case void 0:
16589
+ default:
16590
+ return takeTail(input.screenText ?? "", lines);
16591
+ }
16689
16592
  }
16690
- function buildCliParseInput(options) {
16691
- const {
16692
- accumulatedBuffer,
16693
- accumulatedRawBuffer,
16694
- recentOutputBuffer,
16695
- terminalScreenText,
16696
- workingDir,
16697
- providerSessionId,
16698
- historySessionId,
16699
- baseMessages,
16700
- partialResponse,
16701
- isWaitingForResponse,
16702
- scope,
16703
- runtimeSettings,
16704
- spawnAt
16705
- } = options;
16706
- const buffer = scope ? sliceFromOffset(accumulatedBuffer, scope.bufferStart) : accumulatedBuffer;
16707
- const rawBuffer = scope ? sliceFromOffset(accumulatedRawBuffer, scope.rawBufferStart) : accumulatedRawBuffer;
16708
- const screenText = terminalScreenText;
16709
- const recentBuffer = buffer.slice(-1e3) || recentOutputBuffer;
16710
- return {
16711
- buffer,
16712
- rawBuffer,
16713
- recentBuffer,
16714
- screenText,
16715
- workspace: workingDir,
16716
- workingDir,
16717
- providerSessionId,
16718
- historySessionId,
16719
- screen: buildCliScreenSnapshot(screenText),
16720
- bufferScreen: buildCliScreenSnapshot(buffer),
16721
- recentScreen: buildCliScreenSnapshot(recentBuffer),
16722
- messages: [...baseMessages],
16723
- partialResponse,
16724
- isWaitingForResponse,
16725
- promptText: scope?.prompt || "",
16726
- settings: { ...runtimeSettings },
16727
- ...typeof spawnAt === "number" && spawnAt > 0 ? { spawnAt } : {}
16728
- };
16593
+ function compileSpinnerMatchers(spec) {
16594
+ return spec.patterns.map((p) => compile2(p.regex, p.flags ?? "i"));
16729
16595
  }
16730
- function summarizeCliTraceText(text, max = 800) {
16731
- const value = sanitizeTerminalText(String(text || ""));
16732
- if (value.length <= max) return value;
16733
- return `\u2026${value.slice(-max)}`;
16596
+ function compileSettledPromptMatchers(spec) {
16597
+ const prompt = compile2(spec.regex, spec.flags ?? "m");
16598
+ const footers = (spec.withFooter ?? []).map((f) => {
16599
+ if (f.kind === "regex") {
16600
+ const re = compile2(f.pattern, f.flags ?? "i");
16601
+ return { test: (s2) => re.test(s2) };
16602
+ }
16603
+ const needle = f.pattern.toLowerCase();
16604
+ return { test: (s2) => s2.toLowerCase().includes(needle) };
16605
+ });
16606
+ return { prompt, footers };
16734
16607
  }
16735
- var init_provider_cli_parse = __esm({
16736
- "src/cli-adapters/provider-cli-parse.ts"() {
16737
- "use strict";
16738
- init_provider_cli_shared();
16739
- }
16608
+ function extractButtonLabels(spec, text) {
16609
+ if (!text) return [];
16610
+ const flags = spec.buttonFlags && spec.buttonFlags.includes("m") ? spec.buttonFlags : `${spec.buttonFlags ?? ""}m`;
16611
+ const buttonRe = compile2(spec.buttonPattern, flags);
16612
+ const labelGroup = Number.isInteger(spec.buttonLabelGroup) && (spec.buttonLabelGroup ?? 0) > 0 ? spec.buttonLabelGroup : 1;
16613
+ const out = [];
16614
+ for (const line of text.split("\n")) {
16615
+ buttonRe.lastIndex = 0;
16616
+ const m = buttonRe.exec(line);
16617
+ if (!m) continue;
16618
+ const captured = m[labelGroup] ?? (labelGroup === 1 && m.length > 2 ? m[m.length - 1] : void 0);
16619
+ if (captured && captured.trim()) out.push(captured.trim());
16620
+ }
16621
+ return out;
16622
+ }
16623
+ function buttonBlockApprovalCue(spec, text) {
16624
+ const labels = extractButtonLabels(spec, text);
16625
+ if (labels.length < 2) return false;
16626
+ if (pickApprovalButton(labels).index < 0) return false;
16627
+ return hasNegativeApprovalOption(labels);
16628
+ }
16629
+ function modalMatches(spec, input) {
16630
+ const text = input.screenText ?? "";
16631
+ const question = compile2(spec.questionPattern, spec.questionFlags ?? "i");
16632
+ if (question.test(text)) return true;
16633
+ for (const variant of spec.questionVariants ?? []) {
16634
+ const re = compile2(variant.regex, variant.flags ?? "i");
16635
+ if (re.test(text)) return true;
16636
+ }
16637
+ if (buttonBlockApprovalCue(spec, text)) return true;
16638
+ return false;
16639
+ }
16640
+ function evaluateGroup(group, spec, input, compiled) {
16641
+ switch (group) {
16642
+ case "spinner": {
16643
+ if (!spec.spinner || !compiled.spinner) return null;
16644
+ const text = scopeText(input, spec.spinner.scope, spec.spinner.scopeWindowLines);
16645
+ return compiled.spinner.some((re) => re.test(text)) ? "generating" : null;
16646
+ }
16647
+ case "modal": {
16648
+ if (!spec.modal) return null;
16649
+ return modalMatches(spec.modal, input) ? "waiting_approval" : null;
16650
+ }
16651
+ case "settled-prompt": {
16652
+ if (!spec.settledPrompt || !compiled.settled) return null;
16653
+ const text = scopeText(input, spec.settledPrompt.scope, spec.settledPrompt.scopeWindowLines);
16654
+ if (!compiled.settled.prompt.test(text)) return null;
16655
+ if (compiled.settled.footers.length === 0) return "idle";
16656
+ return compiled.settled.footers.every((f) => f.test(text)) ? "idle" : null;
16657
+ }
16658
+ // Groups declared in the catalog but not yet implemented as builder steps
16659
+ // return null so they are no-ops in dispatch — declaring them in `order`
16660
+ // is forward-compatible. Phase 2 Week 8+ will wire them.
16661
+ case "cue-ordering":
16662
+ case "error-detection":
16663
+ case "approval-stitching":
16664
+ return null;
16665
+ default:
16666
+ return null;
16667
+ }
16668
+ }
16669
+ function buildDetectStatusFromTui(spec) {
16670
+ const compiledSpinner = spec.spinner ? compileSpinnerMatchers(spec.spinner) : null;
16671
+ const compiledSettled = spec.settledPrompt ? compileSettledPromptMatchers(spec.settledPrompt) : null;
16672
+ const compiled = { spinner: compiledSpinner, settled: compiledSettled };
16673
+ const order = spec.dispatchOrder?.order && spec.dispatchOrder.order.length > 0 ? spec.dispatchOrder.order : DEFAULT_ORDER;
16674
+ return function detectStatus(input) {
16675
+ const effectiveInput = spec.visibleRegion ? {
16676
+ ...input,
16677
+ screenText: applyVisibleRegion(spec.visibleRegion, input.screenText ?? ""),
16678
+ tail: applyVisibleRegion(spec.visibleRegion, input.tail)
16679
+ } : input;
16680
+ for (const group of order) {
16681
+ const verdict = evaluateGroup(group, spec, effectiveInput, compiled);
16682
+ if (verdict !== null) return verdict;
16683
+ }
16684
+ return null;
16685
+ };
16686
+ }
16687
+ var DEFAULT_ORDER;
16688
+ var init_detect_status = __esm({
16689
+ "src/providers/sdk/v1/builders/cli/detect-status.ts"() {
16690
+ "use strict";
16691
+ init_visible_region();
16692
+ init_approval_utils();
16693
+ DEFAULT_ORDER = ["spinner", "modal", "settled-prompt"];
16694
+ }
16740
16695
  });
16741
16696
 
16742
- // src/cli-adapters/cli-state-engine.ts
16743
- var SCRIPT_STATUS_DEBOUNCE_MS, MAX_FINISH_RETRIES, FINISH_RETRY_DELAY_MS, MAX_TRACE_ENTRIES, APPROVAL_EXIT_TIMEOUT_MS, IDLE_CONFIRMATION_GRACE_MS, CliStateEngine;
16744
- var init_cli_state_engine = __esm({
16745
- "src/cli-adapters/cli-state-engine.ts"() {
16697
+ // src/providers/sdk/v1/builders/cli/parse-approval.ts
16698
+ function compile3(re, flags) {
16699
+ try {
16700
+ return new RegExp(re, flags);
16701
+ } catch (e) {
16702
+ throw new Error(`Invalid regex /${re}/${flags ?? ""}: ${e.message}`);
16703
+ }
16704
+ }
16705
+ function findQuestionLineIndex(spec, lines) {
16706
+ const primary = compile3(spec.questionPattern, spec.questionFlags ?? "i");
16707
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
16708
+ if (primary.test(lines[i])) return { index: i, matchedSource: "primary" };
16709
+ }
16710
+ for (const variant of spec.questionVariants ?? []) {
16711
+ const re = compile3(variant.regex, variant.flags ?? "i");
16712
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
16713
+ if (re.test(lines[i])) return { index: i, matchedSource: variant.label ?? "variant" };
16714
+ }
16715
+ }
16716
+ return null;
16717
+ }
16718
+ function scopeLines(spec, lines, questionIndex) {
16719
+ const scope = spec.scope ?? "between-last-two-separators";
16720
+ if (scope === "whole-screen") {
16721
+ return { start: 0, end: lines.length };
16722
+ }
16723
+ if (scope === "window-around-question") {
16724
+ const window = spec.scopeWindowLines ?? 16;
16725
+ return {
16726
+ start: Math.max(0, questionIndex - 2),
16727
+ end: Math.min(lines.length, questionIndex + window)
16728
+ };
16729
+ }
16730
+ let lastSep = -1;
16731
+ let prevSep = -1;
16732
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
16733
+ if (SEPARATOR_RE.test(lines[i].trim())) {
16734
+ if (lastSep < 0) lastSep = i;
16735
+ else if (prevSep < 0) {
16736
+ prevSep = i;
16737
+ break;
16738
+ }
16739
+ }
16740
+ }
16741
+ if (lastSep >= 0 && prevSep >= 0) {
16742
+ return { start: prevSep, end: lastSep + 1 };
16743
+ }
16744
+ return {
16745
+ start: Math.max(0, questionIndex - 4),
16746
+ end: Math.min(lines.length, questionIndex + 16)
16747
+ };
16748
+ }
16749
+ function extractButtons(spec, lines, windowStart, windowEnd) {
16750
+ const buttonRe = compile3(spec.buttonPattern, spec.buttonFlags ?? "m");
16751
+ const out = [];
16752
+ const labelGroup = Number.isInteger(spec.buttonLabelGroup) && (spec.buttonLabelGroup ?? 0) > 0 ? spec.buttonLabelGroup : 1;
16753
+ let i = windowStart;
16754
+ while (i < windowEnd) {
16755
+ const line = lines[i];
16756
+ const m = buttonRe.exec(line);
16757
+ const captured = m?.[labelGroup] ?? (labelGroup === 1 && m && m.length > 2 ? m[m.length - 1] : void 0);
16758
+ if (m && captured) {
16759
+ let label = captured.trim();
16760
+ if (spec.continuationLines) {
16761
+ let j = i + 1;
16762
+ while (j < windowEnd) {
16763
+ const next = lines[j];
16764
+ if (!next.trim()) break;
16765
+ if (buttonRe.test(next)) break;
16766
+ if (!/^\s+/.test(next)) break;
16767
+ label += " " + next.trim();
16768
+ j += 1;
16769
+ }
16770
+ i = j;
16771
+ } else {
16772
+ i += 1;
16773
+ }
16774
+ out.push(label);
16775
+ } else {
16776
+ i += 1;
16777
+ }
16778
+ }
16779
+ return out;
16780
+ }
16781
+ function buildMessage(spec, lines, questionIndex, windowStart, windowEnd) {
16782
+ const messageParts = [];
16783
+ if (spec.contextHeader) {
16784
+ const ctxRe = compile3(spec.contextHeader.regex, (spec.contextHeader.flags ?? "i") + "m");
16785
+ const haystack = lines.slice(windowStart, windowEnd).join("\n");
16786
+ const m = ctxRe.exec(haystack);
16787
+ if (m) messageParts.push(m[1] ? m[1].trim() : m[0].trim());
16788
+ }
16789
+ messageParts.push(lines[questionIndex].trim());
16790
+ return messageParts.filter(Boolean).join(" \u2014 ");
16791
+ }
16792
+ function extractInlineButtons(spec, lines, windowStart, windowEnd) {
16793
+ if (!spec.inlineButtonPattern) return [];
16794
+ const declaredFlags = spec.inlineButtonFlags ?? "gi";
16795
+ const flags = declaredFlags.includes("g") ? declaredFlags : declaredFlags + "g";
16796
+ const re = compile3(spec.inlineButtonPattern, flags);
16797
+ const out = [];
16798
+ for (let i = windowStart; i < windowEnd; i += 1) {
16799
+ let m;
16800
+ re.lastIndex = 0;
16801
+ while ((m = re.exec(lines[i])) !== null) {
16802
+ const label = (m[1] ?? m[0]).trim();
16803
+ if (label && !out.includes(label)) out.push(label);
16804
+ if (m.index === re.lastIndex) re.lastIndex += 1;
16805
+ }
16806
+ }
16807
+ return out;
16808
+ }
16809
+ function buildParseApprovalFromTui(spec, visibleRegion) {
16810
+ const minButtons = spec.minButtons ?? 2;
16811
+ return function parseApproval(input) {
16812
+ const rawText = input.screenText ?? input.buffer ?? "";
16813
+ if (!rawText) return null;
16814
+ const text = visibleRegion ? applyVisibleRegion(visibleRegion, rawText) : rawText;
16815
+ const lines = text.split("\n");
16816
+ const question = findQuestionLineIndex(spec, lines);
16817
+ if (!question) return null;
16818
+ const { start, end } = scopeLines(spec, lines, question.index);
16819
+ if (question.index < start || question.index >= end) return null;
16820
+ let buttons = extractButtons(spec, lines, question.index + 1, end);
16821
+ if (buttons.length < minButtons && spec.inlineButtonPattern) {
16822
+ buttons = extractInlineButtons(spec, lines, question.index, end);
16823
+ }
16824
+ if (buttons.length < minButtons) return null;
16825
+ const message = buildMessage(spec, lines, question.index, start, end);
16826
+ return { message, buttons };
16827
+ };
16828
+ }
16829
+ var SEPARATOR_RE;
16830
+ var init_parse_approval = __esm({
16831
+ "src/providers/sdk/v1/builders/cli/parse-approval.ts"() {
16832
+ "use strict";
16833
+ init_visible_region();
16834
+ SEPARATOR_RE = /^(?:─|━|═|━){10,}\s*$/;
16835
+ }
16836
+ });
16837
+
16838
+ // src/providers/sdk/v1/builders/cli/parse-session.ts
16839
+ function stripAnsi2(text) {
16840
+ return String(text || "").replace(/\x1b\[(\d*)C/g, (_m, n) => " ".repeat(Math.max(1, Number(n) || 1))).replace(/\x1b\[\d*D/g, "").replace(ANSI_RE, "").replace(OSC_RE, "").replace(/\x1b[P^_X][\s\S]*?(?:\x07|\x1b\\)/g, "").replace(/\x1b(?:[@-Z\\-_])/g, "");
16841
+ }
16842
+ function splitLines(text) {
16843
+ return stripAnsi2(text).replace(//g, "").split(/\r?\n/).map((l) => l.replace(/\s+$/, ""));
16844
+ }
16845
+ function pickInputText(input, scope) {
16846
+ if (!input) return "";
16847
+ if (scope === "screen") return String(input.screenText || input.screen?.text || "");
16848
+ if (scope === "tail") return String(input.tail || input.recentBuffer || input.buffer || "");
16849
+ return String(input.buffer || input.rawBuffer || input.screenText || "");
16850
+ }
16851
+ function stableHash(value) {
16852
+ return crypto3.createHash("sha1").update(String(value || "")).digest("hex").slice(0, 12);
16853
+ }
16854
+ function normalizeMessageIdentity(messages, status) {
16855
+ const list = Array.isArray(messages) ? messages : [];
16856
+ let turnIndex = -1;
16857
+ return list.map((message, index) => {
16858
+ const role = message?.role || "assistant";
16859
+ const kind = message?.kind || "standard";
16860
+ const content = typeof message?.content === "string" ? message.content : "";
16861
+ if (role === "user" || turnIndex < 0) turnIndex += 1;
16862
+ const seed = [role, kind, "", index, content].join("\n");
16863
+ const providerUnitKey = `v2-pty:${role}:${kind}:${index}:${stableHash(seed)}`;
16864
+ const bubbleId = `bubble:${providerUnitKey}`;
16865
+ const turnKey = `turn:${turnIndex}`;
16866
+ const isStreamingTail = status === "generating" && role === "assistant" && index === list.length - 1;
16867
+ return {
16868
+ ...message,
16869
+ providerUnitKey,
16870
+ bubbleId,
16871
+ sequence: index,
16872
+ _turnKey: turnKey,
16873
+ bubbleState: isStreamingTail ? "streaming" : "final"
16874
+ };
16875
+ });
16876
+ }
16877
+ function buildParseSessionFromTui(spec) {
16878
+ if (!spec.transcriptPty) {
16879
+ throw new Error("buildParseSessionFromTui: spec.transcriptPty is required");
16880
+ }
16881
+ const assistantRe = new RegExp(spec.transcriptPty.assistantPrefix.regex, spec.transcriptPty.assistantPrefix.flags || "");
16882
+ const userRe = spec.transcriptPty.userPrefix ? new RegExp(spec.transcriptPty.userPrefix.regex, spec.transcriptPty.userPrefix.flags || "") : null;
16883
+ const toolRe = spec.transcriptPty.toolPrefix ? new RegExp(spec.transcriptPty.toolPrefix.regex, spec.transcriptPty.toolPrefix.flags || "") : null;
16884
+ const toolSkip = spec.transcriptPty.toolPrefix?.skip ?? false;
16885
+ const chromeRes = (spec.transcriptPty.chromePatterns || []).map((p) => new RegExp(p.regex, p.flags || ""));
16886
+ const requireIndentForContinuation = spec.transcriptPty.continuationLine?.indented ?? false;
16887
+ const stripLeadingChrome = spec.transcriptPty.stripLeadingChrome ?? true;
16888
+ const scope = spec.transcriptPty.scope ?? "buffer";
16889
+ const detectStatus = spec.spinner || spec.settledPrompt || spec.modal || spec.dispatchOrder ? buildDetectStatusFromTui({
16890
+ spinner: spec.spinner,
16891
+ settledPrompt: spec.settledPrompt,
16892
+ modal: spec.modal,
16893
+ dispatchOrder: spec.dispatchOrder
16894
+ }) : () => null;
16895
+ const parseApproval = spec.modal ? buildParseApprovalFromTui(spec.modal) : () => null;
16896
+ const sessionIdRe = spec.sessionIdExtraction ? new RegExp(
16897
+ spec.sessionIdExtraction.regex,
16898
+ spec.sessionIdExtraction.flags ?? "i"
16899
+ ) : null;
16900
+ const sessionIdScope = spec.sessionIdExtraction?.scope ?? "tail";
16901
+ return function parseSession(input) {
16902
+ const status = detectStatus(input) ?? "idle";
16903
+ const modal = parseApproval(input);
16904
+ const text = pickInputText(input, scope);
16905
+ const lines = splitLines(text);
16906
+ const messages = [];
16907
+ let seenFirstRoleLine = !stripLeadingChrome;
16908
+ for (const raw of lines) {
16909
+ const line = raw;
16910
+ if (line.trim() === "") {
16911
+ continue;
16912
+ }
16913
+ let isChrome = false;
16914
+ for (const cre of chromeRes) {
16915
+ if (cre.test(line)) {
16916
+ isChrome = true;
16917
+ break;
16918
+ }
16919
+ }
16920
+ if (isChrome) continue;
16921
+ const userMatch = userRe ? line.match(userRe) : null;
16922
+ const toolMatch = toolRe ? line.match(toolRe) : null;
16923
+ const assistMatch = line.match(assistantRe);
16924
+ if (userMatch) {
16925
+ seenFirstRoleLine = true;
16926
+ const content = (userMatch[1] ?? userMatch[0]).trim();
16927
+ if (content) messages.push({ role: "user", kind: "standard", content });
16928
+ continue;
16929
+ }
16930
+ if (toolMatch) {
16931
+ seenFirstRoleLine = true;
16932
+ if (toolSkip) continue;
16933
+ const content = (toolMatch[1] ?? toolMatch[0]).trim();
16934
+ if (content) messages.push({ role: "assistant", kind: "tool", content });
16935
+ continue;
16936
+ }
16937
+ if (assistMatch) {
16938
+ seenFirstRoleLine = true;
16939
+ const content = (assistMatch[1] ?? assistMatch[0]).trim();
16940
+ if (content) messages.push({ role: "assistant", kind: "standard", content });
16941
+ continue;
16942
+ }
16943
+ if (!seenFirstRoleLine) continue;
16944
+ if (requireIndentForContinuation && !/^\s/.test(line)) continue;
16945
+ const last = messages[messages.length - 1];
16946
+ if (!last) continue;
16947
+ const cont = line.replace(/^\s+/, "");
16948
+ if (cont) last.content = last.content ? `${last.content}
16949
+ ${cont}` : cont;
16950
+ }
16951
+ const result = {
16952
+ status,
16953
+ messages,
16954
+ activeModal: modal,
16955
+ modal,
16956
+ parsedStatus: status
16957
+ };
16958
+ if (sessionIdRe) {
16959
+ const haystack = stripAnsi2(pickInputText(input, sessionIdScope));
16960
+ const match = haystack.match(sessionIdRe);
16961
+ const captured = match?.[1]?.trim();
16962
+ if (captured) result.providerSessionId = captured;
16963
+ }
16964
+ return result;
16965
+ };
16966
+ }
16967
+ var crypto3, ANSI_RE, OSC_RE;
16968
+ var init_parse_session = __esm({
16969
+ "src/providers/sdk/v1/builders/cli/parse-session.ts"() {
16970
+ "use strict";
16971
+ init_detect_status();
16972
+ init_parse_approval();
16973
+ crypto3 = __toESM(require("crypto"));
16974
+ ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
16975
+ OSC_RE = /\x1b\][^\x07\x1b\n]*(?:\x07|\x1b\\|(?=\n|$))/g;
16976
+ }
16977
+ });
16978
+
16979
+ // src/cli-adapters/cli-script-runner.ts
16980
+ function summarizeInput(input) {
16981
+ const screenText = typeof input?.screenText === "string" ? input.screenText : "";
16982
+ const rawBuffer = typeof input?.rawBuffer === "string" ? input.rawBuffer : "";
16983
+ const tail = typeof input?.tail === "string" ? input.tail : "";
16984
+ return {
16985
+ screenTextLen: screenText.length,
16986
+ rawBufferLen: rawBuffer.length,
16987
+ tailLen: tail.length,
16988
+ isWaitingForResponse: typeof input?.isWaitingForResponse === "boolean" ? input.isWaitingForResponse : void 0,
16989
+ screenTextHead: screenText ? screenText.slice(0, 200) : void 0
16990
+ };
16991
+ }
16992
+ function summarizeResult(result) {
16993
+ try {
16994
+ const json = JSON.stringify(result);
16995
+ return json && json.length > 400 ? `${json.slice(0, 400)}\u2026[truncated ${json.length - 400}]` : json ?? "undefined";
16996
+ } catch (e) {
16997
+ return `<unserializable: ${e?.message || e}>`;
16998
+ }
16999
+ }
17000
+ var TRACE_RING_CAPACITY, DEFAULT_SCRIPT_CALL_BUDGET_MS, BUDGET_WARN_THROTTLE_MS, CliScriptRunner;
17001
+ var init_cli_script_runner = __esm({
17002
+ "src/cli-adapters/cli-script-runner.ts"() {
16746
17003
  "use strict";
16747
17004
  init_logger();
16748
- init_provider_cli_parse();
16749
17005
  init_provider_cli_shared();
16750
- SCRIPT_STATUS_DEBOUNCE_MS = 3e3;
16751
- MAX_FINISH_RETRIES = 2;
16752
- FINISH_RETRY_DELAY_MS = 300;
16753
- MAX_TRACE_ENTRIES = 250;
16754
- APPROVAL_EXIT_TIMEOUT_MS = 6e4;
16755
- IDLE_CONFIRMATION_GRACE_MS = 2e3;
16756
- CliStateEngine = class {
16757
- constructor(provider, runner, transport, callbacks, timeouts) {
16758
- this.provider = provider;
16759
- this.runner = runner;
16760
- this.transport = transport;
16761
- this.callbacks = callbacks;
16762
- this.timeouts = timeouts;
17006
+ init_detect_status();
17007
+ init_parse_approval();
17008
+ init_parse_session();
17009
+ TRACE_RING_CAPACITY = 64;
17010
+ DEFAULT_SCRIPT_CALL_BUDGET_MS = 50;
17011
+ BUDGET_WARN_THROTTLE_MS = 3e4;
17012
+ CliScriptRunner = class {
17013
+ scripts = {};
17014
+ scriptState = null;
17015
+ _parseErrorMessage = null;
17016
+ cliType;
17017
+ sdk = {};
17018
+ invocationTrace = [];
17019
+ /** Per-invocation wall-clock budget (ms). Configurable via setScriptCallBudget. */
17020
+ scriptCallBudgetMs = DEFAULT_SCRIPT_CALL_BUDGET_MS;
17021
+ /** Last WARN emit time per scriptName, used to throttle repeated budget violations. */
17022
+ lastBudgetWarnAt = /* @__PURE__ */ new Map();
17023
+ constructor(cliType) {
17024
+ this.cliType = cliType;
17025
+ }
17026
+ /** Returns the most-recent script invocation traces (oldest → newest). */
17027
+ getInvocationTrace() {
17028
+ return this.invocationTrace.slice();
17029
+ }
17030
+ /** Clear the trace ring — used by tests and after PTY reset. */
17031
+ clearInvocationTrace() {
17032
+ this.invocationTrace = [];
16763
17033
  }
16764
- // ── Status ───────────────────────────────────────
16765
- currentStatus = "starting";
16766
- isWaitingForResponse = false;
16767
- currentTurnScope = null;
16768
- activeModal = null;
16769
- // ── Approval ─────────────────────────────────────
16770
- lastApprovalResolvedAt = 0;
16771
- lastResolvedModalMessage = "";
16772
17034
  /**
16773
- * Monotonic counter bumped every time the FSM *enters* waiting_approval
16774
- * with a freshly captured modal (see `applyWaitingApproval`). It is the
16775
- * single discriminator between "the same approval re-observed across TUI
16776
- * paint flaps" and "a genuinely new, distinct approval".
16777
- *
16778
- * The message-equality cooldown below (`lastResolvedModalMessage`) cannot
16779
- * tell these apart on its own: claude-cli routinely presents consecutive
16780
- * approvals whose modal message text is identical (e.g. two back-to-back
16781
- * Bash-command prompts). When that second approval arrived inside
16782
- * `approvalCooldown`, the message-equality guard silently swallowed the
16783
- * key write and the approval stuck forever — fatal under auto-approval.
17035
+ * Configure the wall-clock budget (ms) applied to every script invocation.
16784
17036
  *
16785
- * `approvalEntrySeq` increments on every fresh entry; `lastResolvedEntrySeq`
16786
- * records which entry the cooldown belongs to. We only short-circuit the
16787
- * write when we are still resolving *that same* entry a new entry (new
16788
- * seq) is always a real, distinct approval and must be written.
16789
- */
16790
- approvalEntrySeq = 0;
16791
- lastResolvedEntrySeq = -1;
16792
- /**
16793
- * When the engine previously held a modal but the latest parse failed
16794
- * to extract one, we record the timestamp here and only drop the modal
16795
- * after the configured `approvalCooldown` to avoid flapping between
16796
- * waiting_approval and generating on every Claude TUI redraw — that
16797
- * flapping is what fed auto-approve a fresh modal signature on each
16798
- * paint and made the engine type "1" repeatedly into the prompt.
17037
+ * Out-of-range or non-finite values are clamped to [1, 5000] and the
17038
+ * default (50ms) is used as a fallback. The budget is enforced per-call,
17039
+ * not aggregated it does not abort a runaway script (Node CJS cannot
17040
+ * interrupt synchronous code without a worker thread). Instead, an
17041
+ * exceeded budget flags the trace entry with `timedOut: true` and emits
17042
+ * a throttled WARN naming the script and elapsed time so an operator
17043
+ * can identify which provider is hanging the settle loop.
16799
17044
  */
16800
- modalLostAt = 0;
16801
- approvalExitTimeout = null;
16802
- // ── Response tracking ────────────────────────────
16803
- responseEpoch = 0;
16804
- submitPendingUntil = 0;
16805
- responseSettleIgnoreUntil = 0;
16806
- submitRetryUsed = false;
16807
- submitRetryPromptSnippet = "";
16808
- finishRetryCount = 0;
16809
- providerErrorMessage = null;
16810
- providerErrorReason = null;
16811
- // ── Timers ───────────────────────────────────────
16812
- settleTimer = null;
16813
- idleTimeout = null;
16814
- finishRetryTimer = null;
16815
- providerErrorRetryTimer = null;
16816
- providerErrorRetryKey = "";
16817
- // ── Debounce ─────────────────────────────────────
16818
- pendingScriptStatus = null;
16819
- pendingScriptStatusSince = 0;
16820
- pendingScriptStatusTimer = null;
16821
- // ── Idle candidate ───────────────────────────────
16822
- idleFinishCandidate = null;
16823
- // ── Idle confirmation grace ──────────────────────
16824
- /**
16825
- * `finishResponse` produces the `generating → idle` transition that
16826
- * coordinators interpret as "task complete". Some providers (antigravity-
16827
- * cli observed in the wild) briefly paint a screen that looks like an
16828
- * idle prompt between tool result frames while still actively running,
16829
- * which fired `response_finished` and broke completion semantics.
16830
- * We defer the actual idle transition by IDLE_CONFIRMATION_GRACE_MS and
16831
- * cancel it if the scripted detection re-detects generating during that
16832
- * window — a true completion stays idle for many seconds, so a 2-second
17045
+ setScriptCallBudget(ms) {
17046
+ if (typeof ms !== "number" || !Number.isFinite(ms)) {
17047
+ this.scriptCallBudgetMs = DEFAULT_SCRIPT_CALL_BUDGET_MS;
17048
+ return;
17049
+ }
17050
+ const clamped = Math.max(1, Math.min(5e3, Math.floor(ms)));
17051
+ this.scriptCallBudgetMs = clamped;
17052
+ }
17053
+ /** Test/debug accessor — current effective budget in ms. */
17054
+ getScriptCallBudgetMs() {
17055
+ return this.scriptCallBudgetMs;
17056
+ }
17057
+ recordTrace(entry) {
17058
+ this.invocationTrace.push(entry);
17059
+ if (this.invocationTrace.length > TRACE_RING_CAPACITY) {
17060
+ this.invocationTrace.splice(0, this.invocationTrace.length - TRACE_RING_CAPACITY);
17061
+ }
17062
+ }
17063
+ // ─── Script lifecycle ─────────────────────────────
17064
+ setScripts(scripts, providerTui) {
17065
+ this.sdk = this.buildSdk(providerTui);
17066
+ const tui = providerTui;
17067
+ const enriched = { ...scripts };
17068
+ if (typeof enriched.detectStatus !== "function" && this.sdk.declarativeDetectStatus) {
17069
+ enriched.detectStatus = this.sdk.declarativeDetectStatus;
17070
+ }
17071
+ if (typeof enriched.parseApproval !== "function" && this.sdk.declarativeParseApproval) {
17072
+ enriched.parseApproval = this.sdk.declarativeParseApproval;
17073
+ }
17074
+ if (typeof enriched.parseSession !== "function" && tui?.transcriptPty) {
17075
+ try {
17076
+ const synth = buildParseSessionFromTui({
17077
+ spinner: tui.spinner,
17078
+ settledPrompt: tui.settledPrompt,
17079
+ modal: tui.modal,
17080
+ dispatchOrder: tui.dispatchOrder,
17081
+ transcriptPty: tui.transcriptPty
17082
+ });
17083
+ enriched.parseSession = ((input) => {
17084
+ const out = synth(input);
17085
+ return {
17086
+ ...out,
17087
+ messages: normalizeMessageIdentity(out.messages, out.status ?? "idle")
17088
+ };
17089
+ });
17090
+ } catch (e) {
17091
+ LOG.warn("CLI", `[${this.cliType}] buildParseSessionFromTui failed: ${e?.message || e}`);
17092
+ }
17093
+ }
17094
+ this.scripts = enriched;
17095
+ this._parseErrorMessage = null;
17096
+ this.scriptState = typeof enriched.createState === "function" ? enriched.createState() ?? null : null;
17097
+ }
17098
+ buildSdk(providerTui) {
17099
+ const tui = providerTui;
17100
+ if (!tui) return {};
17101
+ const sdk = {};
17102
+ if (tui.spinner || tui.settledPrompt || tui.modal || tui.dispatchOrder) {
17103
+ try {
17104
+ sdk.declarativeDetectStatus = buildDetectStatusFromTui({
17105
+ spinner: tui.spinner,
17106
+ settledPrompt: tui.settledPrompt,
17107
+ modal: tui.modal,
17108
+ dispatchOrder: tui.dispatchOrder
17109
+ });
17110
+ } catch (e) {
17111
+ LOG.warn("CLI", `[${this.cliType}] buildDetectStatusFromTui failed: ${e?.message || e}`);
17112
+ }
17113
+ }
17114
+ if (tui.modal) {
17115
+ try {
17116
+ sdk.declarativeParseApproval = buildParseApprovalFromTui(tui.modal);
17117
+ } catch (e) {
17118
+ LOG.warn("CLI", `[${this.cliType}] buildParseApprovalFromTui failed: ${e?.message || e}`);
17119
+ }
17120
+ }
17121
+ return sdk;
17122
+ }
17123
+ /** Reset per-session state — called when the PTY process exits. */
17124
+ resetSessionState() {
17125
+ this.scriptState = null;
17126
+ this.invocationTrace = [];
17127
+ this.lastBudgetWarnAt.clear();
17128
+ }
17129
+ // ─── Script access (for reflection and test patching) ────────────────────
17130
+ /** Returns the live scripts object. Direct property assignment on this object
17131
+ * patches individual scripts without replacing others (used in tests). */
17132
+ get cliScripts() {
17133
+ return this.scripts;
17134
+ }
17135
+ // ─── Capability checks ────────────────────────────
17136
+ hasDetectStatus() {
17137
+ return typeof this.scripts.detectStatus === "function";
17138
+ }
17139
+ hasParseSession() {
17140
+ return typeof this.scripts.parseSession === "function";
17141
+ }
17142
+ getScriptNames() {
17143
+ return listCliScriptNames(this.scripts);
17144
+ }
17145
+ // ─── Error state ──────────────────────────────────
17146
+ get parseErrorMessage() {
17147
+ return this._parseErrorMessage;
17148
+ }
17149
+ clearParseError() {
17150
+ this._parseErrorMessage = null;
17151
+ }
17152
+ // ─── Script invocation ────────────────────────────
17153
+ detectStatus(input) {
17154
+ if (!this.scripts.detectStatus) return null;
17155
+ try {
17156
+ return this.invoke("detectStatus", this.scripts.detectStatus, input);
17157
+ } catch (e) {
17158
+ LOG.warn("CLI", `[${this.cliType}] detectStatus error: ${e?.message || e}`);
17159
+ return null;
17160
+ }
17161
+ }
17162
+ parseApproval(input) {
17163
+ if (!this.scripts.parseApproval) return null;
17164
+ try {
17165
+ return this.invoke(
17166
+ "parseApproval",
17167
+ this.scripts.parseApproval,
17168
+ input
17169
+ );
17170
+ } catch (e) {
17171
+ LOG.warn("CLI", `[${this.cliType}] parseApproval error: ${e?.message || e}`);
17172
+ return null;
17173
+ }
17174
+ }
17175
+ parseSession(input) {
17176
+ if (!this.scripts.parseSession) {
17177
+ this._parseErrorMessage = `${this.cliType} parseSession unavailable`;
17178
+ return null;
17179
+ }
17180
+ try {
17181
+ const result = this.invoke("parseSession", this.scripts.parseSession, input);
17182
+ this._parseErrorMessage = null;
17183
+ return result && typeof result === "object" ? result : null;
17184
+ } catch (e) {
17185
+ this._parseErrorMessage = e?.message || String(e);
17186
+ LOG.warn("CLI", `[${this.cliType}] parseSession error: ${this._parseErrorMessage}`);
17187
+ return null;
17188
+ }
17189
+ }
17190
+ /**
17191
+ * Invoke an arbitrary named script (e.g. setModel, openModelPicker).
17192
+ * Throws if the script is not available.
17193
+ */
17194
+ invokeByName(name, input) {
17195
+ const fn = this.scripts[name];
17196
+ if (typeof fn !== "function") {
17197
+ throw new Error(`CLI script '${name}' not available`);
17198
+ }
17199
+ return this.invoke(name, fn, input);
17200
+ }
17201
+ // ─── Internal ─────────────────────────────────────
17202
+ invoke(scriptName, fn, input) {
17203
+ const arity = fn.length;
17204
+ const startedAt = Date.now();
17205
+ const startedHr = typeof process !== "undefined" && typeof process.hrtime === "function" ? process.hrtime.bigint() : null;
17206
+ let result;
17207
+ try {
17208
+ if (arity >= 3) {
17209
+ result = fn(this.scriptState, input, this.sdk);
17210
+ } else if (arity === 2) {
17211
+ result = fn(this.scriptState, input);
17212
+ } else {
17213
+ result = fn(input);
17214
+ }
17215
+ const elapsedUs = startedHr ? Number((process.hrtime.bigint() - startedHr) / 1000n) : 0;
17216
+ const timedOut = this.checkBudget(scriptName, elapsedUs);
17217
+ this.recordTrace({
17218
+ at: startedAt,
17219
+ scriptName,
17220
+ arity,
17221
+ inputSummary: summarizeInput(input),
17222
+ ok: true,
17223
+ elapsedUs,
17224
+ resultSummary: summarizeResult(result),
17225
+ ...timedOut ? { timedOut: true } : {}
17226
+ });
17227
+ return result;
17228
+ } catch (e) {
17229
+ const elapsedUs = startedHr ? Number((process.hrtime.bigint() - startedHr) / 1000n) : 0;
17230
+ const timedOut = this.checkBudget(scriptName, elapsedUs);
17231
+ this.recordTrace({
17232
+ at: startedAt,
17233
+ scriptName,
17234
+ arity,
17235
+ inputSummary: summarizeInput(input),
17236
+ ok: false,
17237
+ elapsedUs,
17238
+ error: e?.message ? String(e.message).slice(0, 400) : String(e).slice(0, 400),
17239
+ ...timedOut ? { timedOut: true } : {}
17240
+ });
17241
+ throw e;
17242
+ }
17243
+ }
17244
+ /**
17245
+ * Returns true when `elapsedUs` exceeded the configured budget. On the
17246
+ * first violation per script (or after the throttle window expires) we
17247
+ * emit a single WARN so the operator learns which provider is slow.
17248
+ *
17249
+ * We deliberately throttle per-scriptName so a chronically-slow
17250
+ * detectStatus doesn't spam the log on every PTY frame settle.
17251
+ */
17252
+ checkBudget(scriptName, elapsedUs) {
17253
+ const budgetUs = this.scriptCallBudgetMs * 1e3;
17254
+ if (elapsedUs <= budgetUs) return false;
17255
+ const now = Date.now();
17256
+ const last = this.lastBudgetWarnAt.get(scriptName) ?? 0;
17257
+ if (now - last >= BUDGET_WARN_THROTTLE_MS) {
17258
+ this.lastBudgetWarnAt.set(scriptName, now);
17259
+ LOG.warn(
17260
+ "CLI",
17261
+ `[${this.cliType}] script ${scriptName} took ${elapsedUs}us, budget ${budgetUs}us`
17262
+ );
17263
+ }
17264
+ return true;
17265
+ }
17266
+ };
17267
+ }
17268
+ });
17269
+
17270
+ // src/cli-adapters/provider-cli-parse.ts
17271
+ function sliceFromOffset(text, start) {
17272
+ if (!text) return "";
17273
+ if (!Number.isFinite(start) || start <= 0) return text;
17274
+ if (start >= text.length) return "";
17275
+ return text.slice(start);
17276
+ }
17277
+ function normalizeCliParsedMessages(parsedMessages, _options) {
17278
+ return Array.isArray(parsedMessages) ? parsedMessages : [];
17279
+ }
17280
+ function buildCliParseInput(options) {
17281
+ const {
17282
+ accumulatedBuffer,
17283
+ accumulatedRawBuffer,
17284
+ recentOutputBuffer,
17285
+ terminalScreenText,
17286
+ workingDir,
17287
+ providerSessionId,
17288
+ historySessionId,
17289
+ baseMessages,
17290
+ partialResponse,
17291
+ isWaitingForResponse,
17292
+ scope,
17293
+ runtimeSettings,
17294
+ spawnAt
17295
+ } = options;
17296
+ const buffer = scope ? sliceFromOffset(accumulatedBuffer, scope.bufferStart) : accumulatedBuffer;
17297
+ const rawBuffer = scope ? sliceFromOffset(accumulatedRawBuffer, scope.rawBufferStart) : accumulatedRawBuffer;
17298
+ const screenText = terminalScreenText;
17299
+ const recentBuffer = buffer.slice(-1e3) || recentOutputBuffer;
17300
+ return {
17301
+ buffer,
17302
+ rawBuffer,
17303
+ recentBuffer,
17304
+ screenText,
17305
+ workspace: workingDir,
17306
+ workingDir,
17307
+ providerSessionId,
17308
+ historySessionId,
17309
+ screen: buildCliScreenSnapshot(screenText),
17310
+ bufferScreen: buildCliScreenSnapshot(buffer),
17311
+ recentScreen: buildCliScreenSnapshot(recentBuffer),
17312
+ messages: [...baseMessages],
17313
+ partialResponse,
17314
+ isWaitingForResponse,
17315
+ promptText: scope?.prompt || "",
17316
+ settings: { ...runtimeSettings },
17317
+ ...typeof spawnAt === "number" && spawnAt > 0 ? { spawnAt } : {}
17318
+ };
17319
+ }
17320
+ function summarizeCliTraceText(text, max = 800) {
17321
+ const value = sanitizeTerminalText(String(text || ""));
17322
+ if (value.length <= max) return value;
17323
+ return `\u2026${value.slice(-max)}`;
17324
+ }
17325
+ var init_provider_cli_parse = __esm({
17326
+ "src/cli-adapters/provider-cli-parse.ts"() {
17327
+ "use strict";
17328
+ init_provider_cli_shared();
17329
+ }
17330
+ });
17331
+
17332
+ // src/cli-adapters/cli-state-engine.ts
17333
+ var SCRIPT_STATUS_DEBOUNCE_MS, MAX_FINISH_RETRIES, FINISH_RETRY_DELAY_MS, MAX_TRACE_ENTRIES, APPROVAL_EXIT_TIMEOUT_MS, IDLE_CONFIRMATION_GRACE_MS, CliStateEngine;
17334
+ var init_cli_state_engine = __esm({
17335
+ "src/cli-adapters/cli-state-engine.ts"() {
17336
+ "use strict";
17337
+ init_logger();
17338
+ init_provider_cli_parse();
17339
+ init_provider_cli_shared();
17340
+ SCRIPT_STATUS_DEBOUNCE_MS = 3e3;
17341
+ MAX_FINISH_RETRIES = 2;
17342
+ FINISH_RETRY_DELAY_MS = 300;
17343
+ MAX_TRACE_ENTRIES = 250;
17344
+ APPROVAL_EXIT_TIMEOUT_MS = 6e4;
17345
+ IDLE_CONFIRMATION_GRACE_MS = 2e3;
17346
+ CliStateEngine = class {
17347
+ constructor(provider, runner, transport, callbacks, timeouts) {
17348
+ this.provider = provider;
17349
+ this.runner = runner;
17350
+ this.transport = transport;
17351
+ this.callbacks = callbacks;
17352
+ this.timeouts = timeouts;
17353
+ }
17354
+ // ── Status ───────────────────────────────────────
17355
+ currentStatus = "starting";
17356
+ isWaitingForResponse = false;
17357
+ currentTurnScope = null;
17358
+ activeModal = null;
17359
+ // ── Approval ─────────────────────────────────────
17360
+ lastApprovalResolvedAt = 0;
17361
+ lastResolvedModalMessage = "";
17362
+ /**
17363
+ * Monotonic counter bumped every time the FSM *enters* waiting_approval
17364
+ * with a freshly captured modal (see `applyWaitingApproval`). It is the
17365
+ * single discriminator between "the same approval re-observed across TUI
17366
+ * paint flaps" and "a genuinely new, distinct approval".
17367
+ *
17368
+ * The message-equality cooldown below (`lastResolvedModalMessage`) cannot
17369
+ * tell these apart on its own: claude-cli routinely presents consecutive
17370
+ * approvals whose modal message text is identical (e.g. two back-to-back
17371
+ * Bash-command prompts). When that second approval arrived inside
17372
+ * `approvalCooldown`, the message-equality guard silently swallowed the
17373
+ * key write and the approval stuck forever — fatal under auto-approval.
17374
+ *
17375
+ * `approvalEntrySeq` increments on every fresh entry; `lastResolvedEntrySeq`
17376
+ * records which entry the cooldown belongs to. We only short-circuit the
17377
+ * write when we are still resolving *that same* entry — a new entry (new
17378
+ * seq) is always a real, distinct approval and must be written.
17379
+ */
17380
+ approvalEntrySeq = 0;
17381
+ lastResolvedEntrySeq = -1;
17382
+ /**
17383
+ * When the engine previously held a modal but the latest parse failed
17384
+ * to extract one, we record the timestamp here and only drop the modal
17385
+ * after the configured `approvalCooldown` to avoid flapping between
17386
+ * waiting_approval and generating on every Claude TUI redraw — that
17387
+ * flapping is what fed auto-approve a fresh modal signature on each
17388
+ * paint and made the engine type "1" repeatedly into the prompt.
17389
+ */
17390
+ modalLostAt = 0;
17391
+ approvalExitTimeout = null;
17392
+ // ── Response tracking ────────────────────────────
17393
+ responseEpoch = 0;
17394
+ submitPendingUntil = 0;
17395
+ responseSettleIgnoreUntil = 0;
17396
+ submitRetryUsed = false;
17397
+ submitRetryPromptSnippet = "";
17398
+ finishRetryCount = 0;
17399
+ providerErrorMessage = null;
17400
+ providerErrorReason = null;
17401
+ // ── Timers ───────────────────────────────────────
17402
+ settleTimer = null;
17403
+ idleTimeout = null;
17404
+ finishRetryTimer = null;
17405
+ providerErrorRetryTimer = null;
17406
+ providerErrorRetryKey = "";
17407
+ // ── Debounce ─────────────────────────────────────
17408
+ pendingScriptStatus = null;
17409
+ pendingScriptStatusSince = 0;
17410
+ pendingScriptStatusTimer = null;
17411
+ // ── Idle candidate ───────────────────────────────
17412
+ idleFinishCandidate = null;
17413
+ // ── Idle confirmation grace ──────────────────────
17414
+ /**
17415
+ * `finishResponse` produces the `generating → idle` transition that
17416
+ * coordinators interpret as "task complete". Some providers (antigravity-
17417
+ * cli observed in the wild) briefly paint a screen that looks like an
17418
+ * idle prompt between tool result frames while still actively running,
17419
+ * which fired `response_finished` and broke completion semantics.
17420
+ * We defer the actual idle transition by IDLE_CONFIRMATION_GRACE_MS and
17421
+ * cancel it if the scripted detection re-detects generating during that
17422
+ * window — a true completion stays idle for many seconds, so a 2-second
16833
17423
  * grace is sufficient to filter the paint blip.
16834
17424
  */
16835
17425
  pendingIdleFinishTimer = null;
@@ -19534,596 +20124,6 @@ ${lastSnapshot}`;
19534
20124
  }
19535
20125
  });
19536
20126
 
19537
- // src/providers/spec/evaluator.ts
19538
- var evaluator_exports = {};
19539
- __export(evaluator_exports, {
19540
- evaluateCondition: () => evaluateCondition,
19541
- extractButtonsFromRule: () => extractButtonsFromRule,
19542
- extractTitle: () => extractTitle,
19543
- lastContiguousNumberedBlock: () => lastContiguousNumberedBlock,
19544
- resolveSections: () => resolveSections,
19545
- sectionText: () => sectionText
19546
- });
19547
- function resolveSize(size, total) {
19548
- if (size === void 0) return 0;
19549
- if (typeof size === "number") return Math.max(0, Math.min(total, size));
19550
- const m = /^(\d+(?:\.\d+)?)%$/.exec(size);
19551
- if (!m) return 0;
19552
- const pct = Number(m[1]);
19553
- return Math.max(0, Math.min(total, Math.round(total * pct / 100)));
19554
- }
19555
- function resolveSections(sectionsObj, lines) {
19556
- const total = lines.length;
19557
- const anchored = /* @__PURE__ */ new Map();
19558
- const sectionEntries = Object.entries(sectionsObj);
19559
- for (const [id, sec] of sectionEntries) {
19560
- let from = 0;
19561
- let to = total;
19562
- if (sec.anchor !== void 0) {
19563
- try {
19564
- const anchorPatterns = Array.isArray(sec.anchor) ? sec.anchor : [sec.anchor];
19565
- const sharedCtx = Array.isArray(sec.anchor_context) ? null : sec.anchor_context ?? null;
19566
- const ctxList = Array.isArray(sec.anchor_context) ? sec.anchor_context : anchorPatterns.map(() => sharedCtx);
19567
- const candidates = anchorPatterns.map((pattern, k) => {
19568
- const ctx = ctxList[k] ?? null;
19569
- return {
19570
- re: new RegExp(pattern, sec.anchor_flags ?? ""),
19571
- prevRe: ctx?.prev !== void 0 ? new RegExp(ctx.prev, ctx.prev_flags ?? "") : null,
19572
- nextRe: ctx?.next !== void 0 ? new RegExp(ctx.next, ctx.next_flags ?? "") : null
19573
- };
19574
- });
19575
- const matchesCandidate = (c, i) => c.re.test(lines[i]) && (c.prevRe === null || i > 0 && c.prevRe.test(lines[i - 1])) && (c.nextRe === null || i < total - 1 && c.nextRe.test(lines[i + 1]));
19576
- let idx = -1;
19577
- for (const c of candidates) {
19578
- let candIdx = -1;
19579
- if (sec.anchor_last) {
19580
- for (let i = total - 1; i >= 0; i--) {
19581
- if (matchesCandidate(c, i)) {
19582
- candIdx = i;
19583
- break;
19584
- }
19585
- }
19586
- } else {
19587
- for (let i = 0; i < total; i++) {
19588
- if (matchesCandidate(c, i)) {
19589
- candIdx = i;
19590
- break;
19591
- }
19592
- }
19593
- }
19594
- if (candIdx !== -1 && (idx === -1 || candIdx < idx)) idx = candIdx;
19595
- }
19596
- if (idx !== -1) {
19597
- from = idx;
19598
- to = total;
19599
- if (sec.until_regex !== void 0) {
19600
- try {
19601
- const ure = new RegExp(sec.until_regex, sec.until_regex_flags ?? "");
19602
- const end = lines.findIndex((l, i) => i > idx && ure.test(l));
19603
- if (end !== -1) to = end;
19604
- } catch {
19605
- }
19606
- } else if (sec.lines !== void 0) {
19607
- to = Math.min(total, from + sec.lines);
19608
- }
19609
- }
19610
- } catch {
19611
- }
19612
- } else if (sec.from_top !== void 0) {
19613
- from = resolveSize(sec.from_top, total);
19614
- to = total;
19615
- } else if (sec.from_bottom !== void 0) {
19616
- const sz = resolveSize(sec.from_bottom, total);
19617
- from = total - sz;
19618
- to = total;
19619
- }
19620
- anchored.set(id, { fromLine: from, toLine: to });
19621
- }
19622
- const resolved = [];
19623
- for (const [id, sec] of sectionEntries) {
19624
- let { fromLine, toLine } = anchored.get(id);
19625
- if (sec.until !== void 0) {
19626
- if (sec.until.startsWith("^")) {
19627
- try {
19628
- const ure = new RegExp(sec.until);
19629
- const end = lines.findIndex((l, i) => i > fromLine && ure.test(l));
19630
- if (end !== -1) toLine = end;
19631
- } catch {
19632
- }
19633
- } else {
19634
- const target = anchored.get(sec.until);
19635
- if (target) toLine = target.fromLine;
19636
- }
19637
- }
19638
- if (toLine < fromLine) toLine = fromLine;
19639
- const text = lines.slice(fromLine, toLine).join("\n");
19640
- resolved.push({ id, fromLine, toLine, text });
19641
- }
19642
- return resolved;
19643
- }
19644
- function sectionText(sections, sectionId, fullScreen) {
19645
- if (!sectionId) return fullScreen;
19646
- const found = sections.find((s2) => s2.id === sectionId);
19647
- return found ? found.text : "";
19648
- }
19649
- function isRegexCondition(c) {
19650
- return "matches" in c;
19651
- }
19652
- function isChangedCondition(c) {
19653
- return "cursor_above" in c && "changed" in c;
19654
- }
19655
- function isAllCondition(c) {
19656
- return "all" in c;
19657
- }
19658
- function isAnyCondition(c) {
19659
- return "any" in c;
19660
- }
19661
- function evaluateCondition(cond, sections, fullScreen, cursor, prevLines, trace, stateId) {
19662
- if (isAllCondition(cond)) {
19663
- for (const child of cond.all) {
19664
- if (!evaluateCondition(child, sections, fullScreen, cursor, prevLines, trace, stateId)) {
19665
- return false;
19666
- }
19667
- }
19668
- return true;
19669
- }
19670
- if (isAnyCondition(cond)) {
19671
- for (const child of cond.any) {
19672
- if (evaluateCondition(child, sections, fullScreen, cursor, prevLines, trace, stateId)) {
19673
- return true;
19674
- }
19675
- }
19676
- return false;
19677
- }
19678
- if (isChangedCondition(cond)) {
19679
- if (!cursor || !prevLines || prevLines.length === 0) return false;
19680
- const curLines = fullScreen.split("\n");
19681
- const startRow = Math.max(0, cursor.row - cond.cursor_above);
19682
- const endRow = cursor.row;
19683
- const currentSlice = curLines.slice(startRow, endRow).join("\n");
19684
- const prevSlice = prevLines.slice(startRow, endRow).join("\n");
19685
- const didChange = currentSlice !== prevSlice;
19686
- const result = cond.changed ? didChange : !didChange;
19687
- const stableSuffix = cond.stable_ms != null ? ` stable_ms=${cond.stable_ms}` : "";
19688
- trace.push({
19689
- kind: result ? "state_match" : "state_skip",
19690
- text: `state[${stateId}] changed cond cursor_above=${cond.cursor_above} rows[${startRow},${endRow}) changed=${didChange} expected=${cond.changed}${stableSuffix} result=${result}`
19691
- });
19692
- return result;
19693
- }
19694
- if (isRegexCondition(cond)) {
19695
- const haystack = sectionText(sections, cond.section, fullScreen);
19696
- let matched = false;
19697
- try {
19698
- const re = new RegExp(cond.matches, cond.flags ?? "i");
19699
- matched = re.test(haystack);
19700
- } catch {
19701
- matched = false;
19702
- }
19703
- if (!matched) {
19704
- trace.push({ kind: "state_skip", text: `state[${stateId}] regex cond ${cond.section ?? "*"}~/${cond.matches}/ no match` });
19705
- return false;
19706
- }
19707
- if (cursor !== void 0) {
19708
- if (cond.cursor_row_min !== void 0 && cursor.row < cond.cursor_row_min) {
19709
- trace.push({ kind: "state_skip", text: `state[${stateId}] cursor row ${cursor.row} < cursor_row_min ${cond.cursor_row_min}` });
19710
- return false;
19711
- }
19712
- if (cond.cursor_row_max !== void 0 && cursor.row > cond.cursor_row_max) {
19713
- trace.push({ kind: "state_skip", text: `state[${stateId}] cursor row ${cursor.row} > cursor_row_max ${cond.cursor_row_max}` });
19714
- return false;
19715
- }
19716
- if (cond.cursor_col_min !== void 0 && cursor.col < cond.cursor_col_min) {
19717
- trace.push({ kind: "state_skip", text: `state[${stateId}] cursor col ${cursor.col} < cursor_col_min ${cond.cursor_col_min}` });
19718
- return false;
19719
- }
19720
- if (cond.cursor_col_max !== void 0 && cursor.col > cond.cursor_col_max) {
19721
- trace.push({ kind: "state_skip", text: `state[${stateId}] cursor col ${cursor.col} > cursor_col_max ${cond.cursor_col_max}` });
19722
- return false;
19723
- }
19724
- }
19725
- trace.push({ kind: "state_match", text: `state[${stateId}] regex cond ${cond.section ?? "*"}~/${cond.matches}/ matched${cursor !== void 0 ? ` cursor=(${cursor.row},${cursor.col})` : ""}` });
19726
- return true;
19727
- }
19728
- return false;
19729
- }
19730
- function extractTitle(rule, sections, fullScreen) {
19731
- const hay = sectionText(sections, rule.section, fullScreen);
19732
- if (!hay) return null;
19733
- if (rule.first_line) {
19734
- const lines = hay.split("\n");
19735
- for (const line of lines) {
19736
- const stripped = line.trim();
19737
- if (stripped && !/^[─╌═─\s]+$/.test(stripped)) {
19738
- return stripped;
19739
- }
19740
- }
19741
- return null;
19742
- }
19743
- if (rule.regex) {
19744
- try {
19745
- const re = new RegExp(rule.regex, rule.flags ?? "i");
19746
- const m = re.exec(hay);
19747
- if (m) return (m[1] ?? m[0]).trim();
19748
- } catch {
19749
- }
19750
- }
19751
- return null;
19752
- }
19753
- function compilePattern(ref) {
19754
- const flags = ref.flags ?? "gm";
19755
- return new RegExp(ref.pattern, flags.includes("g") ? flags : flags + "g");
19756
- }
19757
- function compileLinePattern(ref) {
19758
- const flags = (ref.flags ?? "m").replace(/g/g, "");
19759
- return new RegExp(ref.pattern, flags);
19760
- }
19761
- function extractButtonsFromRule(rule, hay) {
19762
- const keyTemplate = rule.key_for_index;
19763
- const continuationLines = rule.continuation_lines ?? false;
19764
- const buttons = [];
19765
- if (continuationLines) {
19766
- const re = compileLinePattern(rule);
19767
- const lines = hay.split("\n");
19768
- for (let i = 0; i < lines.length; i += 1) {
19769
- const m = re.exec(lines[i]);
19770
- if (!m) continue;
19771
- const idx = Number(m[1]);
19772
- let label = String(m[2] ?? "").trim();
19773
- if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
19774
- const current = hasCursorMarker(lines[i]);
19775
- let j = i + 1;
19776
- while (j < lines.length) {
19777
- const next = lines[j];
19778
- if (!next.trim()) break;
19779
- if (re.test(next)) break;
19780
- if (!/^\s+/.test(next)) break;
19781
- label += " " + next.trim();
19782
- j += 1;
19783
- }
19784
- const key = keyTemplate.replace(/\{index\}/g, String(idx));
19785
- buttons.push({ index: idx, label, key, current });
19786
- i = j - 1;
19787
- }
19788
- } else {
19789
- const re = compilePattern(rule);
19790
- let m;
19791
- while ((m = re.exec(hay)) !== null) {
19792
- const idx = Number(m[1]);
19793
- const label = String(m[2] ?? "").trim();
19794
- if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
19795
- const key = keyTemplate.replace(/\{index\}/g, String(idx));
19796
- buttons.push({ index: idx, label, key, current: hasCursorMarker(m[0]) });
19797
- }
19798
- }
19799
- const block2 = lastContiguousNumberedBlock(buttons);
19800
- block2.sort((a, b) => a.index - b.index);
19801
- return block2;
19802
- }
19803
- function lastContiguousNumberedBlock(entries) {
19804
- if (entries.length <= 1) return entries.slice();
19805
- let start = entries.length - 1;
19806
- for (let i = entries.length - 1; i > 0; i -= 1) {
19807
- if (entries[i - 1].index === entries[i].index - 1) start = i - 1;
19808
- else break;
19809
- }
19810
- return entries.slice(start);
19811
- }
19812
- function hasCursorMarker(text) {
19813
- return /^\s*[❯›>]/.test(text);
19814
- }
19815
- var init_evaluator = __esm({
19816
- "src/providers/spec/evaluator.ts"() {
19817
- "use strict";
19818
- }
19819
- });
19820
-
19821
- // src/providers/spec/fsm-types.ts
19822
- function isV4Spec(raw) {
19823
- return !!raw && typeof raw === "object" && raw.$schema === "adhdev:cli/spec@4";
19824
- }
19825
- function initialState(spec) {
19826
- return spec.states.find((s2) => s2.initial) ?? spec.states[0];
19827
- }
19828
- function stateById(spec, id) {
19829
- return spec.states.find((s2) => s2.id === id);
19830
- }
19831
- function outgoingTransitions(spec, stateId) {
19832
- const matches = spec.transitions.filter((t) => {
19833
- if (t.from === "*") return true;
19834
- if (Array.isArray(t.from)) return t.from.includes(stateId);
19835
- return t.from === stateId;
19836
- });
19837
- return matches.map((t, i) => ({ t, i })).sort((a, b) => (b.t.priority ?? 0) - (a.t.priority ?? 0) || a.i - b.i).map((x) => x.t);
19838
- }
19839
- function statusForState(state) {
19840
- if (state.status) return state.status;
19841
- if (state.modal) return "approval";
19842
- if (state.id === "busy" || state.id === "generating") return "generating";
19843
- return "idle";
19844
- }
19845
- function modalKindForState(state) {
19846
- if (state.modal_kind) return state.modal_kind;
19847
- if (state.modal) return "approval";
19848
- return null;
19849
- }
19850
- var init_fsm_types = __esm({
19851
- "src/providers/spec/fsm-types.ts"() {
19852
- "use strict";
19853
- }
19854
- });
19855
-
19856
- // src/providers/spec/fsm-evaluator.ts
19857
- var fsm_evaluator_exports = {};
19858
- __export(fsm_evaluator_exports, {
19859
- evaluateConditionPreview: () => evaluateConditionPreview,
19860
- evaluateFsm: () => evaluateFsm
19861
- });
19862
- function regionKey(cursorAbove) {
19863
- return cursorAbove && cursorAbove > 0 ? cursorAbove : WHOLE_SCREEN;
19864
- }
19865
- function isRegex(c) {
19866
- return "matches" in c;
19867
- }
19868
- function isChanged(c) {
19869
- return "cursor_above" in c && "changed" in c;
19870
- }
19871
- function isElapsed(c) {
19872
- return "elapsed_ms" in c;
19873
- }
19874
- function isStable(c) {
19875
- return "stable_ms" in c;
19876
- }
19877
- function isAll(c) {
19878
- return "all" in c;
19879
- }
19880
- function isAny(c) {
19881
- return "any" in c;
19882
- }
19883
- function isNot(c) {
19884
- return "not" in c;
19885
- }
19886
- function evalCond(cond, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId) {
19887
- if (isAll(cond)) {
19888
- const children = cond.all.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
19889
- const result = children.every((c) => c.result);
19890
- const remainingMs = result ? 0 : Math.max(0, ...children.filter((c) => !c.result).map((c) => c.remainingMs ?? 0));
19891
- return { kind: "all", result, detail: `all(${children.length})`, remainingMs, children };
19892
- }
19893
- if (isAny(cond)) {
19894
- const children = cond.any.map((c) => evalCond(c, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId));
19895
- const result = children.some((c) => c.result);
19896
- const pending = children.filter((c) => !c.result).map((c) => c.remainingMs ?? Infinity);
19897
- const remainingMs = result ? 0 : pending.length ? Math.min(...pending) : 0;
19898
- return { kind: "any", result, detail: `any(${children.length})`, remainingMs: Number.isFinite(remainingMs) ? remainingMs : 0, children };
19899
- }
19900
- if (isNot(cond)) {
19901
- const child = evalCond(cond.not, sections, fullScreen, cursor, prevLines, clock, legacyTrace, stateId);
19902
- return { kind: "not", result: !child.result, detail: `not`, remainingMs: 0, children: [child] };
19903
- }
19904
- if (isElapsed(cond)) {
19905
- const age = clock.now - clock.stateEnteredAt;
19906
- const result = age >= cond.elapsed_ms;
19907
- const remainingMs = result ? 0 : cond.elapsed_ms - age;
19908
- return { kind: "elapsed", result, detail: `elapsed ${age}ms / ${cond.elapsed_ms}ms`, remainingMs };
19909
- }
19910
- if (isStable(cond)) {
19911
- const key = regionKey(cond.cursor_above);
19912
- const lastChanged = clock.regionLastChangedAt.get(key) ?? clock.stateEnteredAt;
19913
- const stableFor = clock.now - lastChanged;
19914
- const result = stableFor >= cond.stable_ms;
19915
- const remainingMs = result ? 0 : cond.stable_ms - stableFor;
19916
- const where = key === WHOLE_SCREEN ? "screen" : `cursor_above=${cond.cursor_above}`;
19917
- return { kind: "stable", result, detail: `stable ${where} ${stableFor}ms / ${cond.stable_ms}ms`, remainingMs };
19918
- }
19919
- if (isRegex(cond) || isChanged(cond)) {
19920
- const result = evaluateCondition(cond, sections, fullScreen, cursor, prevLines, legacyTrace, stateId);
19921
- const kind = isRegex(cond) ? "regex" : "changed";
19922
- const detail = isRegex(cond) ? `${cond.section ?? "*"}~/${cond.matches}/` : `cursor_above=${cond.cursor_above} changed=${cond.changed}`;
19923
- let matchedText;
19924
- if (result && isRegex(cond)) {
19925
- try {
19926
- const hay = sectionText(sections, cond.section, fullScreen);
19927
- const re = new RegExp(cond.matches, cond.flags ?? "i");
19928
- const m = re.exec(hay);
19929
- if (m && m[0]) matchedText = m[0].replace(/\s+/g, " ").trim().slice(0, 160);
19930
- } catch {
19931
- }
19932
- }
19933
- return matchedText ? { kind, result, detail, matchedText } : { kind, result, detail };
19934
- }
19935
- return { kind: "all", result: false, detail: "unknown condition" };
19936
- }
19937
- function fromLabel(t) {
19938
- const from = Array.isArray(t.from) ? t.from.join("|") : t.from;
19939
- return t.label ?? `${from}\u2192${t.to}`;
19940
- }
19941
- function evaluateFsm(spec, currentStateId, screenText, cursor, prevLines, clock) {
19942
- const legacyTrace = [];
19943
- const lines = screenText.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
19944
- const cleanScreen = lines.join("\n");
19945
- const sections = resolveSections(spec.sections ?? {}, lines);
19946
- const outgoing = outgoingTransitions(spec, currentStateId);
19947
- const transitions = [];
19948
- let fired = null;
19949
- for (const t of outgoing) {
19950
- const holdMs = t.min_hold_ms ?? 0;
19951
- const heldFor = clock.now - clock.stateEnteredAt;
19952
- const holdSatisfied = heldFor >= holdMs;
19953
- const holdRemainingMs = holdSatisfied ? 0 : holdMs - heldFor;
19954
- let cond;
19955
- let condResult = true;
19956
- if (t.when) {
19957
- cond = evalCond(t.when, sections, cleanScreen, cursor, prevLines, clock, legacyTrace, `${currentStateId}\u2192${t.to}`);
19958
- condResult = cond.result;
19959
- }
19960
- const fires = holdSatisfied && condResult;
19961
- const te = {
19962
- to: t.to,
19963
- label: fromLabel(t),
19964
- eligible: true,
19965
- holdSatisfied,
19966
- holdRemainingMs,
19967
- condResult,
19968
- cond,
19969
- fires,
19970
- priority: t.priority ?? 0
19971
- };
19972
- transitions.push(te);
19973
- if (fires && !fired) fired = te;
19974
- }
19975
- if (fired && !stateById(spec, fired.to)) {
19976
- legacyTrace.push({ kind: "state_skip", text: `transition target ${fired.to} not found` });
19977
- fired = null;
19978
- }
19979
- return { sections, transitions, fired, trace: legacyTrace };
19980
- }
19981
- function evaluateConditionPreview(cond, sections, screenText, cursor) {
19982
- const lines = screenText.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
19983
- const cleanScreen = lines.join("\n");
19984
- const resolved = resolveSections(sections ?? {}, lines);
19985
- const clock = { now: 0, stateEnteredAt: 0, regionLastChangedAt: /* @__PURE__ */ new Map() };
19986
- return evalCond(cond, resolved, cleanScreen, cursor, void 0, clock, [], "preview");
19987
- }
19988
- var WHOLE_SCREEN;
19989
- var init_fsm_evaluator = __esm({
19990
- "src/providers/spec/fsm-evaluator.ts"() {
19991
- "use strict";
19992
- init_evaluator();
19993
- init_fsm_types();
19994
- WHOLE_SCREEN = -1;
19995
- }
19996
- });
19997
-
19998
- // src/providers/spec/fsm-loader.ts
19999
- var fsm_loader_exports = {};
20000
- __export(fsm_loader_exports, {
20001
- loadFsmSpec: () => loadFsmSpec,
20002
- validateFsmSpec: () => validateFsmSpec
20003
- });
20004
- function loadFsmSpec(sourcePath) {
20005
- let raw;
20006
- try {
20007
- raw = JSON.parse(fs10.readFileSync(sourcePath, "utf8"));
20008
- } catch (err) {
20009
- return { ok: false, errors: [`Failed to read/parse spec: ${err.message}`], sourcePath };
20010
- }
20011
- const errors = validateFsmSpec(raw);
20012
- if (errors.length) return { ok: false, errors, sourcePath };
20013
- return { ok: true, spec: raw, sourcePath };
20014
- }
20015
- function validateFsmSpec(raw) {
20016
- const errs = [];
20017
- if (!isV4Spec(raw)) return ['$schema must be "adhdev:cli/spec@4"'];
20018
- const spec = raw;
20019
- if (!spec.id) errs.push("id is required");
20020
- if (!spec.binary) errs.push("binary is required");
20021
- if (!spec.send_message?.submit_key) errs.push("send_message.submit_key is required");
20022
- if (spec.pre_launch_trust !== void 0) {
20023
- const t = spec.pre_launch_trust;
20024
- if (!t || typeof t !== "object" || Array.isArray(t)) {
20025
- errs.push("pre_launch_trust must be an object");
20026
- } else {
20027
- if (typeof t.settings_path !== "string" || !t.settings_path) errs.push("pre_launch_trust.settings_path is required");
20028
- if (typeof t.key !== "string" || !t.key) errs.push("pre_launch_trust.key is required");
20029
- }
20030
- }
20031
- if (spec.refocus_when_stalled_ms !== void 0) {
20032
- if (typeof spec.refocus_when_stalled_ms !== "number" || !(spec.refocus_when_stalled_ms > 0)) {
20033
- errs.push("refocus_when_stalled_ms must be a positive number");
20034
- } else if (!Array.isArray(spec.send_on_spawn) || spec.send_on_spawn.length === 0) {
20035
- errs.push("refocus_when_stalled_ms requires send_on_spawn (the wake sequence to re-inject)");
20036
- }
20037
- }
20038
- if (!Array.isArray(spec.states) || spec.states.length === 0) {
20039
- errs.push("states[] must be a non-empty array");
20040
- return errs;
20041
- }
20042
- if (!Array.isArray(spec.transitions)) {
20043
- errs.push("transitions[] must be an array");
20044
- return errs;
20045
- }
20046
- const ids = /* @__PURE__ */ new Set();
20047
- let initialCount = 0;
20048
- for (const [i, s2] of spec.states.entries()) {
20049
- if (!s2.id) {
20050
- errs.push(`states[${i}].id is required`);
20051
- continue;
20052
- }
20053
- if (ids.has(s2.id)) errs.push(`states[${i}].id "${s2.id}" is duplicated`);
20054
- ids.add(s2.id);
20055
- if (!s2.label) errs.push(`states[${i}].label is required`);
20056
- if (s2.initial) initialCount += 1;
20057
- if (s2.status && !["idle", "generating", "approval"].includes(s2.status)) {
20058
- errs.push(`states[${i}].status "${s2.status}" must be idle|generating|approval`);
20059
- }
20060
- }
20061
- if (initialCount === 0) errs.push("exactly one state must have initial:true (none found)");
20062
- if (initialCount > 1) errs.push(`exactly one state must have initial:true (${initialCount} found)`);
20063
- const sectionIds = new Set(Object.keys(spec.sections ?? {}));
20064
- for (const [i, t] of spec.transitions.entries()) {
20065
- const froms = t.from === "*" ? [] : Array.isArray(t.from) ? t.from : [t.from];
20066
- for (const f of froms) {
20067
- if (!ids.has(f)) errs.push(`transitions[${i}].from references unknown state "${f}"`);
20068
- }
20069
- if (t.from !== "*" && froms.length === 0) errs.push(`transitions[${i}].from is required`);
20070
- if (!t.to) errs.push(`transitions[${i}].to is required`);
20071
- else if (!ids.has(t.to)) errs.push(`transitions[${i}].to references unknown state "${t.to}"`);
20072
- if (t.when) errs.push(...validateCondition(t.when, sectionIds, `transitions[${i}].when`));
20073
- }
20074
- for (const [i, s2] of spec.states.entries()) {
20075
- const sec = s2.extract?.title?.section;
20076
- if (sec && !sectionIds.has(sec)) errs.push(`states[${i}].extract.title.section "${sec}" unknown`);
20077
- const bsec = s2.extract?.buttons?.section;
20078
- if (bsec && !sectionIds.has(bsec)) errs.push(`states[${i}].extract.buttons.section "${bsec}" unknown`);
20079
- }
20080
- return errs;
20081
- }
20082
- function validateCondition(c, sectionIds, path42) {
20083
- const errs = [];
20084
- const w = c;
20085
- if ("all" in w) {
20086
- w.all.forEach((x, i) => errs.push(...validateCondition(x, sectionIds, `${path42}.all[${i}]`)));
20087
- return errs;
20088
- }
20089
- if ("any" in w) {
20090
- w.any.forEach((x, i) => errs.push(...validateCondition(x, sectionIds, `${path42}.any[${i}]`)));
20091
- return errs;
20092
- }
20093
- if ("not" in w) {
20094
- errs.push(...validateCondition(w.not, sectionIds, `${path42}.not`));
20095
- return errs;
20096
- }
20097
- if ("matches" in w) {
20098
- if (w.section && !sectionIds.has(w.section)) errs.push(`${path42}.section "${w.section}" unknown`);
20099
- try {
20100
- new RegExp(w.matches, w.flags ?? "i");
20101
- } catch (e) {
20102
- errs.push(`${path42}.matches invalid regex: ${e.message}`);
20103
- }
20104
- return errs;
20105
- }
20106
- if ("cursor_above" in w && "changed" in w) return errs;
20107
- if ("elapsed_ms" in w) {
20108
- if (typeof w.elapsed_ms !== "number") errs.push(`${path42}.elapsed_ms must be a number`);
20109
- return errs;
20110
- }
20111
- if ("stable_ms" in w) {
20112
- if (typeof w.stable_ms !== "number") errs.push(`${path42}.stable_ms must be a number`);
20113
- return errs;
20114
- }
20115
- errs.push(`${path42} is not a recognized condition`);
20116
- return errs;
20117
- }
20118
- var fs10;
20119
- var init_fsm_loader = __esm({
20120
- "src/providers/spec/fsm-loader.ts"() {
20121
- "use strict";
20122
- fs10 = __toESM(require("fs"));
20123
- init_fsm_types();
20124
- }
20125
- });
20126
-
20127
20127
  // src/providers/sdk/v1/sandbox/require-whitelist.ts
20128
20128
  var require_whitelist_exports = {};
20129
20129
  __export(require_whitelist_exports, {
@@ -33053,6 +33053,503 @@ ${formatManifestValidationIssues2(validation.issues)}`,
33053
33053
  }
33054
33054
  };
33055
33055
 
33056
+ // src/commands/low-family/session-host.ts
33057
+ function toHostedCliRuntimeDescriptor(record) {
33058
+ if (!record || typeof record !== "object") return null;
33059
+ const runtimeId = typeof record.sessionId === "string" ? record.sessionId : "";
33060
+ const cliType = typeof record.providerType === "string" ? record.providerType : "";
33061
+ const workspace = typeof record.workspace === "string" ? record.workspace : "";
33062
+ if (!runtimeId || !cliType || !workspace) return null;
33063
+ return {
33064
+ runtimeId,
33065
+ runtimeKey: typeof record.runtimeKey === "string" ? record.runtimeKey : void 0,
33066
+ displayName: typeof record.displayName === "string" ? record.displayName : void 0,
33067
+ workspaceLabel: typeof record.workspaceLabel === "string" ? record.workspaceLabel : void 0,
33068
+ lifecycle: typeof record.lifecycle === "string" ? record.lifecycle : void 0,
33069
+ recoveryState: typeof record.meta?.runtimeRecoveryState === "string" ? String(record.meta.runtimeRecoveryState) : null,
33070
+ cliType,
33071
+ workspace,
33072
+ cliArgs: Array.isArray(record.meta?.cliArgs) ? record.meta.cliArgs : [],
33073
+ providerSessionId: typeof record.meta?.providerSessionId === "string" ? String(record.meta.providerSessionId) : void 0
33074
+ };
33075
+ }
33076
+ function getWriteConflictOwnerClientId(error) {
33077
+ const message = typeof error === "string" ? error : error instanceof Error ? error.message : "";
33078
+ const match = /^Write owned by\s+(.+)$/.exec(message.trim());
33079
+ return match?.[1]?.trim() || void 0;
33080
+ }
33081
+ function summarizeSessionHostRecord(result) {
33082
+ if (!result || typeof result !== "object") return {};
33083
+ const record = result;
33084
+ return {
33085
+ runtimeKey: typeof record.runtimeKey === "string" ? record.runtimeKey : void 0,
33086
+ lifecycle: typeof record.lifecycle === "string" ? record.lifecycle : void 0,
33087
+ surfaceKind: getSessionHostSurfaceKind(record),
33088
+ attachedClientCount: Array.isArray(record.attachedClients) ? record.attachedClients.length : void 0,
33089
+ hasWriteOwner: !!record.writeOwner,
33090
+ writeOwnerClientId: typeof record.writeOwner?.clientId === "string" ? record.writeOwner.clientId : void 0
33091
+ };
33092
+ }
33093
+ function summarizeSessionHostRecords(result) {
33094
+ const records = Array.isArray(result) ? result : [];
33095
+ const groups = partitionSessionHostRecords(records);
33096
+ return {
33097
+ sessionCount: records.length,
33098
+ liveRuntimeCount: groups.liveRuntimes.length,
33099
+ recoverySnapshotCount: groups.recoverySnapshots.length,
33100
+ inactiveRecordCount: groups.inactiveRecords.length
33101
+ };
33102
+ }
33103
+ function summarizeSessionHostDiagnostics(result) {
33104
+ const diagnostics = result && typeof result === "object" ? result : {};
33105
+ const sessions = Array.isArray(diagnostics.sessions) ? diagnostics.sessions : [];
33106
+ return {
33107
+ runtimeCount: typeof diagnostics.runtimeCount === "number" ? diagnostics.runtimeCount : void 0,
33108
+ ...summarizeSessionHostRecords(sessions)
33109
+ };
33110
+ }
33111
+ function summarizeSessionHostPruneResult(result) {
33112
+ const value = result && typeof result === "object" ? result : {};
33113
+ return {
33114
+ duplicateGroupCount: typeof value.duplicateGroupCount === "number" ? value.duplicateGroupCount : void 0,
33115
+ prunedCount: Array.isArray(value.prunedSessionIds) ? value.prunedSessionIds.length : void 0,
33116
+ keptCount: Array.isArray(value.keptSessionIds) ? value.keptSessionIds.length : void 0
33117
+ };
33118
+ }
33119
+ async function traceSessionHostAction(action, args, run, summarizeResult2) {
33120
+ const interactionId = typeof args?._interactionId === "string" ? args._interactionId : void 0;
33121
+ const sessionId = typeof args?.sessionId === "string" ? args.sessionId : void 0;
33122
+ const requestedPayload = { action };
33123
+ if (sessionId) requestedPayload.sessionId = sessionId;
33124
+ if (typeof args?.clientId === "string") requestedPayload.clientId = args.clientId;
33125
+ if (typeof args?.signal === "string") requestedPayload.signal = args.signal;
33126
+ if (typeof args?.providerType === "string") requestedPayload.providerType = args.providerType;
33127
+ if (typeof args?.workspace === "string") requestedPayload.workspace = args.workspace;
33128
+ if (typeof args?.dryRun === "boolean") requestedPayload.dryRun = args.dryRun;
33129
+ recordDebugTrace({
33130
+ interactionId,
33131
+ category: "session_host",
33132
+ stage: "action_requested",
33133
+ level: "info",
33134
+ sessionId,
33135
+ payload: requestedPayload
33136
+ });
33137
+ try {
33138
+ const result = await run();
33139
+ recordDebugTrace({
33140
+ interactionId,
33141
+ category: "session_host",
33142
+ stage: "action_result",
33143
+ level: "info",
33144
+ sessionId,
33145
+ payload: {
33146
+ ...requestedPayload,
33147
+ success: true,
33148
+ ...summarizeResult2 ? summarizeResult2(result) : {}
33149
+ }
33150
+ });
33151
+ return result;
33152
+ } catch (error) {
33153
+ recordDebugTrace({
33154
+ interactionId,
33155
+ category: "session_host",
33156
+ stage: "action_failed",
33157
+ level: "error",
33158
+ sessionId,
33159
+ payload: {
33160
+ ...requestedPayload,
33161
+ error: error?.message || String(error),
33162
+ failureKind: getWriteConflictOwnerClientId(error) ? "write_conflict" : "request_failed",
33163
+ conflictOwnerClientId: getWriteConflictOwnerClientId(error)
33164
+ }
33165
+ });
33166
+ throw error;
33167
+ }
33168
+ }
33169
+ var sessionHostHandlers = {
33170
+ session_host_get_diagnostics: async (ctx, args) => {
33171
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33172
+ const diagnostics = await traceSessionHostAction("session_host_get_diagnostics", args, () => ctx.deps.sessionHostControl.getDiagnostics({
33173
+ includeSessions: args?.includeSessions !== false,
33174
+ limit: Number(args?.limit) || void 0
33175
+ }), (result) => ({
33176
+ includeSessions: args?.includeSessions !== false,
33177
+ limit: Number(args?.limit) || void 0,
33178
+ ...summarizeSessionHostDiagnostics(result)
33179
+ }));
33180
+ return { success: true, diagnostics };
33181
+ },
33182
+ session_host_list_sessions: async (ctx, args) => {
33183
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33184
+ const sessions = await traceSessionHostAction("session_host_list_sessions", args, () => ctx.deps.sessionHostControl.listSessions(), (records) => summarizeSessionHostRecords(records));
33185
+ return { success: true, sessions };
33186
+ },
33187
+ session_host_stop_session: async (ctx, args) => {
33188
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33189
+ const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
33190
+ if (!sessionId) return { success: false, error: "sessionId required" };
33191
+ const record = await traceSessionHostAction("session_host_stop_session", args, () => ctx.deps.sessionHostControl.stopSession(sessionId), (result) => summarizeSessionHostRecord(result));
33192
+ return { success: true, record };
33193
+ },
33194
+ session_host_resume_session: async (ctx, args) => {
33195
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33196
+ const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
33197
+ if (!sessionId) return { success: false, error: "sessionId required" };
33198
+ const record = await traceSessionHostAction("session_host_resume_session", args, async () => {
33199
+ const nextRecord = await ctx.deps.sessionHostControl.resumeSession(sessionId);
33200
+ const hosted = toHostedCliRuntimeDescriptor(nextRecord);
33201
+ if (hosted) {
33202
+ await ctx.deps.cliManager.restoreHostedSessions([hosted]);
33203
+ }
33204
+ return nextRecord;
33205
+ }, (result) => ({
33206
+ ...summarizeSessionHostRecord(result),
33207
+ restoredHostedSession: !!toHostedCliRuntimeDescriptor(result)
33208
+ }));
33209
+ return { success: true, record };
33210
+ },
33211
+ session_host_restart_session: async (ctx, args) => {
33212
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33213
+ const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
33214
+ if (!sessionId) return { success: false, error: "sessionId required" };
33215
+ const record = await traceSessionHostAction("session_host_restart_session", args, async () => {
33216
+ const nextRecord = await ctx.deps.sessionHostControl.restartSession(sessionId);
33217
+ const hosted = toHostedCliRuntimeDescriptor(nextRecord);
33218
+ if (hosted) {
33219
+ await ctx.deps.cliManager.restoreHostedSessions([hosted]);
33220
+ }
33221
+ return nextRecord;
33222
+ }, (result) => ({
33223
+ ...summarizeSessionHostRecord(result),
33224
+ restoredHostedSession: !!toHostedCliRuntimeDescriptor(result)
33225
+ }));
33226
+ return { success: true, record };
33227
+ },
33228
+ session_host_send_signal: async (ctx, args) => {
33229
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33230
+ const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
33231
+ const signal = typeof args?.signal === "string" ? args.signal : "";
33232
+ if (!sessionId) return { success: false, error: "sessionId required" };
33233
+ if (!signal) return { success: false, error: "signal required" };
33234
+ const record = await traceSessionHostAction("session_host_send_signal", args, () => ctx.deps.sessionHostControl.sendSignal(sessionId, signal), (result) => summarizeSessionHostRecord(result));
33235
+ return { success: true, record };
33236
+ },
33237
+ session_host_force_detach_client: async (ctx, args) => {
33238
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33239
+ const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
33240
+ const clientId = typeof args?.clientId === "string" ? args.clientId : "";
33241
+ if (!sessionId) return { success: false, error: "sessionId required" };
33242
+ if (!clientId) return { success: false, error: "clientId required" };
33243
+ const record = await traceSessionHostAction("session_host_force_detach_client", args, () => ctx.deps.sessionHostControl.forceDetachClient(sessionId, clientId), (result) => summarizeSessionHostRecord(result));
33244
+ return { success: true, record };
33245
+ },
33246
+ session_host_prune_duplicate_sessions: async (ctx, args) => {
33247
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33248
+ const result = await traceSessionHostAction("session_host_prune_duplicate_sessions", args, () => ctx.deps.sessionHostControl.pruneDuplicateSessions({
33249
+ providerType: typeof args?.providerType === "string" ? args.providerType : void 0,
33250
+ workspace: typeof args?.workspace === "string" ? args.workspace : void 0,
33251
+ dryRun: args?.dryRun === true
33252
+ }), (value) => summarizeSessionHostPruneResult(value));
33253
+ return { success: true, result };
33254
+ },
33255
+ session_host_acquire_write: async (ctx, args) => {
33256
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33257
+ const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
33258
+ const clientId = typeof args?.clientId === "string" ? args.clientId : "";
33259
+ const ownerType = args?.ownerType === "agent" ? "agent" : "user";
33260
+ if (!sessionId) return { success: false, error: "sessionId required" };
33261
+ if (!clientId) return { success: false, error: "clientId required" };
33262
+ const record = await traceSessionHostAction("session_host_acquire_write", args, () => ctx.deps.sessionHostControl.acquireWrite({
33263
+ sessionId,
33264
+ clientId,
33265
+ ownerType,
33266
+ force: args?.force !== false
33267
+ }), (result) => ({
33268
+ ...summarizeSessionHostRecord(result),
33269
+ ownerType
33270
+ }));
33271
+ return { success: true, record };
33272
+ },
33273
+ session_host_release_write: async (ctx, args) => {
33274
+ if (!ctx.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
33275
+ const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
33276
+ const clientId = typeof args?.clientId === "string" ? args.clientId : "";
33277
+ if (!sessionId) return { success: false, error: "sessionId required" };
33278
+ if (!clientId) return { success: false, error: "clientId required" };
33279
+ const record = await traceSessionHostAction("session_host_release_write", args, () => ctx.deps.sessionHostControl.releaseWrite({
33280
+ sessionId,
33281
+ clientId
33282
+ }), (result) => summarizeSessionHostRecord(result));
33283
+ return { success: true, record };
33284
+ }
33285
+ };
33286
+
33287
+ // src/commands/low-family/spec-providerdev.ts
33288
+ function resolveSpecPathInProviders(specPath, fsm, pathm, osm) {
33289
+ let rootReal;
33290
+ try {
33291
+ rootReal = fsm.realpathSync(pathm.join(osm.homedir(), ".adhdev", "providers"));
33292
+ } catch (e) {
33293
+ return { ok: false, error: `providers root unavailable: ${e.message}` };
33294
+ }
33295
+ const resolved = pathm.resolve(specPath);
33296
+ const base = pathm.basename(resolved);
33297
+ if (!/^[\w.-]+\.json$/.test(base)) {
33298
+ return { ok: false, error: "refused: spec file must be a *.json basename" };
33299
+ }
33300
+ let parentReal;
33301
+ try {
33302
+ parentReal = fsm.realpathSync(pathm.dirname(resolved));
33303
+ } catch (e) {
33304
+ return { ok: false, error: `spec directory not found: ${e.message}` };
33305
+ }
33306
+ if (parentReal !== rootReal && !parentReal.startsWith(rootReal + pathm.sep)) {
33307
+ return { ok: false, error: "refused: spec path must be under the providers root" };
33308
+ }
33309
+ const safe = pathm.join(parentReal, base);
33310
+ try {
33311
+ const st = fsm.lstatSync(safe);
33312
+ if (st.isSymbolicLink()) return { ok: false, error: "refused: spec path is a symlink" };
33313
+ } catch {
33314
+ }
33315
+ return { ok: true, path: safe };
33316
+ }
33317
+ var specProviderDevHandlers = {
33318
+ get_spec_debug: async (ctx, args) => {
33319
+ const sessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : typeof args?.sessionId === "string" ? args.sessionId.trim() : "";
33320
+ if (!sessionId) return { success: false, error: "targetSessionId required" };
33321
+ const target = ctx.deps.sessionRegistry.get(sessionId);
33322
+ if (!target) return { success: false, error: "Session not found", sessionId };
33323
+ const adapterObj = ctx.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
33324
+ const snapshot = adapterObj ? typeof adapterObj.getDebugSnapshot === "function" ? adapterObj.getDebugSnapshot() : typeof adapterObj.getDebugState === "function" ? adapterObj.getDebugState() : null : null;
33325
+ return {
33326
+ success: true,
33327
+ sessionId,
33328
+ providerType: target.providerType,
33329
+ isSpecProvider: snapshot !== null,
33330
+ snapshot
33331
+ };
33332
+ },
33333
+ // ── Spec source read/write for the debug panel's live editor.
33334
+ // Lets the dashboard load a session's spec.json, edit it, and save it back —
33335
+ // the driver's fs.watch picks up the change and hot-reloads the FSM with no
33336
+ // restart. Writes are confined to files under ~/.adhdev/providers.
33337
+ get_spec_source: async (ctx, args) => {
33338
+ const fsm = await import("fs");
33339
+ const pathm = await import("path");
33340
+ const osm = await import("os");
33341
+ const sessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : typeof args?.sessionId === "string" ? args.sessionId.trim() : "";
33342
+ let specPath = typeof args?.specPath === "string" ? args.specPath : "";
33343
+ if (!specPath && sessionId) {
33344
+ const target = ctx.deps.sessionRegistry.get(sessionId);
33345
+ const adapterObj = target ? ctx.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter : null;
33346
+ const snap = adapterObj && typeof adapterObj.getDebugSnapshot === "function" ? adapterObj.getDebugSnapshot() : null;
33347
+ specPath = snap?.specPath ?? "";
33348
+ }
33349
+ if (!specPath) return { success: false, error: "specPath or resolvable targetSessionId required" };
33350
+ const safe = resolveSpecPathInProviders(specPath, fsm, pathm, osm);
33351
+ if (!safe.ok) return { success: false, error: safe.error, specPath };
33352
+ try {
33353
+ const content = fsm.readFileSync(safe.path, "utf8");
33354
+ return { success: true, specPath: safe.path, content };
33355
+ } catch (e) {
33356
+ return { success: false, error: `read failed: ${e.message}`, specPath };
33357
+ }
33358
+ },
33359
+ write_spec_source: async (_ctx, args) => {
33360
+ const fsm = await import("fs");
33361
+ const pathm = await import("path");
33362
+ const osm = await import("os");
33363
+ const specPath = typeof args?.specPath === "string" ? args.specPath : "";
33364
+ const content = typeof args?.content === "string" ? args.content : "";
33365
+ if (!specPath) return { success: false, error: "specPath required" };
33366
+ if (!content) return { success: false, error: "content required" };
33367
+ const safe = resolveSpecPathInProviders(specPath, fsm, pathm, osm);
33368
+ if (!safe.ok) return { success: false, error: safe.error };
33369
+ let parsed;
33370
+ try {
33371
+ parsed = JSON.parse(content);
33372
+ } catch (e) {
33373
+ return { success: false, error: `invalid JSON: ${e.message}` };
33374
+ }
33375
+ if (parsed?.$schema === "adhdev:cli/spec@4") {
33376
+ const { validateFsmSpec: validateFsmSpec2 } = await Promise.resolve().then(() => (init_fsm_loader(), fsm_loader_exports));
33377
+ const errs = validateFsmSpec2(parsed);
33378
+ if (errs.length) return { success: false, error: "spec invalid", validationErrors: errs };
33379
+ }
33380
+ try {
33381
+ fsm.writeFileSync(safe.path, content, "utf8");
33382
+ return { success: true, specPath: safe.path };
33383
+ } catch (e) {
33384
+ return { success: false, error: `write failed: ${e.message}` };
33385
+ }
33386
+ },
33387
+ // ── Validate an in-progress spec (string or object) without writing. The form
33388
+ // builder calls this on every change so Save can stay disabled while there
33389
+ // are structural / reference / regex errors.
33390
+ validate_spec: async (_ctx, args) => {
33391
+ let parsed = args?.spec;
33392
+ if (typeof args?.content === "string") {
33393
+ try {
33394
+ parsed = JSON.parse(args.content);
33395
+ } catch (e) {
33396
+ return { success: true, valid: false, errors: [`invalid JSON: ${e.message}`] };
33397
+ }
33398
+ }
33399
+ if (!parsed || typeof parsed !== "object") {
33400
+ return { success: true, valid: false, errors: ["spec must be an object or content string"] };
33401
+ }
33402
+ const schema = parsed.$schema;
33403
+ if (schema === "adhdev:cli/spec@4") {
33404
+ const { validateFsmSpec: validateFsmSpec2 } = await Promise.resolve().then(() => (init_fsm_loader(), fsm_loader_exports));
33405
+ const errors = validateFsmSpec2(parsed);
33406
+ return { success: true, valid: errors.length === 0, errors };
33407
+ }
33408
+ return { success: true, valid: false, errors: [`unsupported $schema "${schema}" \u2014 form builder is v4-only`] };
33409
+ },
33410
+ // ── Evaluate a single condition against a live session's current screen —
33411
+ // powers the editor's "does this match right now?" preview. Returns the
33412
+ // recursive match tree.
33413
+ eval_condition_preview: async (ctx, args) => {
33414
+ const sessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : typeof args?.sessionId === "string" ? args.sessionId.trim() : "";
33415
+ if (!sessionId) return { success: false, error: "targetSessionId required" };
33416
+ if (!args?.condition || typeof args.condition !== "object") return { success: false, error: "condition required" };
33417
+ const target = ctx.deps.sessionRegistry.get(sessionId);
33418
+ if (!target) return { success: false, error: "Session not found", sessionId };
33419
+ const adapterObj = ctx.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
33420
+ const snap = adapterObj && typeof adapterObj.getDebugSnapshot === "function" ? adapterObj.getDebugSnapshot() : null;
33421
+ if (!snap?.screen) return { success: false, error: "no live screen for session" };
33422
+ let sectionsDef;
33423
+ try {
33424
+ const fsm2 = await import("fs");
33425
+ if (snap.specPath) {
33426
+ const raw = JSON.parse(fsm2.readFileSync(snap.specPath, "utf8"));
33427
+ sectionsDef = raw?.sections;
33428
+ }
33429
+ } catch {
33430
+ }
33431
+ const { evaluateConditionPreview: evaluateConditionPreview2 } = await Promise.resolve().then(() => (init_fsm_evaluator(), fsm_evaluator_exports));
33432
+ try {
33433
+ const result = evaluateConditionPreview2(
33434
+ args.condition,
33435
+ sectionsDef,
33436
+ snap.screen,
33437
+ snap.cursorPosition ?? void 0
33438
+ );
33439
+ return { success: true, result, sections: snap.sections ?? null };
33440
+ } catch (e) {
33441
+ return { success: false, error: `eval failed: ${e.message}` };
33442
+ }
33443
+ },
33444
+ // ── Resolve a sections map against a live session's screen — the section
33445
+ // editor's "test" button. Returns, for each section id, the line range + the
33446
+ // text it captures. Accepts an in-progress sections map so it previews
33447
+ // unsaved edits.
33448
+ resolve_section_preview: async (ctx, args) => {
33449
+ const sessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : typeof args?.sessionId === "string" ? args.sessionId.trim() : "";
33450
+ if (!sessionId) return { success: false, error: "targetSessionId required" };
33451
+ if (!args?.sections || typeof args.sections !== "object") return { success: false, error: "sections map required" };
33452
+ const target = ctx.deps.sessionRegistry.get(sessionId);
33453
+ if (!target) return { success: false, error: "Session not found", sessionId };
33454
+ const adapterObj = ctx.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
33455
+ const snap = adapterObj && typeof adapterObj.getDebugSnapshot === "function" ? adapterObj.getDebugSnapshot() : null;
33456
+ if (!snap?.screen) return { success: false, error: "no live screen for session" };
33457
+ const { resolveSections: resolveSections2 } = await Promise.resolve().then(() => (init_evaluator(), evaluator_exports));
33458
+ try {
33459
+ const lines = String(snap.screen).split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
33460
+ const resolved = resolveSections2(args.sections, lines);
33461
+ return {
33462
+ success: true,
33463
+ screenLineCount: lines.length,
33464
+ sections: resolved.map((s2) => ({ id: s2.id, fromLine: s2.fromLine, toLine: s2.toLine, text: s2.text }))
33465
+ };
33466
+ } catch (e) {
33467
+ return { success: false, error: `resolve failed: ${e.message}` };
33468
+ }
33469
+ }
33470
+ };
33471
+
33472
+ // src/commands/low-family/refine-config.ts
33473
+ init_change_impact_config();
33474
+ var refineConfigHandlers = {
33475
+ get_mesh_refine_config_schema: async (_ctx, _args) => {
33476
+ return {
33477
+ success: true,
33478
+ schema: MESH_REFINE_CONFIG_SCHEMA,
33479
+ locations: MESH_REFINE_CONFIG_LOCATIONS,
33480
+ worktreeBootstrap: {
33481
+ schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
33482
+ locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
33483
+ sourceOfTruth: "repo worktree bootstrap config",
33484
+ runBehavior: "When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node."
33485
+ },
33486
+ sourceOfTruth: "repo mesh/refine config",
33487
+ heuristicRole: "suggestions_only_not_execution_path"
33488
+ };
33489
+ },
33490
+ validate_mesh_refine_config: async (_ctx, args) => {
33491
+ const workspace = typeof args?.workspace === "string" ? args.workspace : process.cwd();
33492
+ const mesh = args?.inlineMesh || {};
33493
+ const loaded = args?.config !== void 0 ? { config: args.config, source: "inline", sourceType: "mesh_policy" } : loadMeshRefineConfig(mesh, workspace);
33494
+ const validation = loaded.config ? validateMeshRefineConfig(loaded.config, loaded.source) : { valid: false, errors: [loaded.error || "repo mesh/refine config unavailable"], commands: [], rejectedCommands: [] };
33495
+ return { success: validation.valid, ...loaded, ...validation };
33496
+ },
33497
+ suggest_mesh_refine_config: async (_ctx, args) => {
33498
+ const workspace = typeof args?.workspace === "string" ? args.workspace : process.cwd();
33499
+ const mesh = args?.inlineMesh || {};
33500
+ return {
33501
+ success: true,
33502
+ ...suggestMeshRefineConfig(mesh, workspace),
33503
+ note: "Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config."
33504
+ };
33505
+ },
33506
+ get_mesh_change_impact_config_schema: async (_ctx, _args) => {
33507
+ return {
33508
+ success: true,
33509
+ schema: CHANGE_IMPACT_CONFIG_SCHEMA,
33510
+ locations: CHANGE_IMPACT_CONFIG_LOCATIONS,
33511
+ sourceOfTruth: "repo change-impact config",
33512
+ heuristicRole: "suggestions_only_not_execution_path",
33513
+ note: "Declarative config only \u2014 JSON/YAML are parsed but never executed. Defines which package/file changes require a daemon rebuild/restart vs. a web-only redeploy vs. nothing."
33514
+ };
33515
+ },
33516
+ validate_mesh_change_impact_config: async (_ctx, args) => {
33517
+ const workspace = typeof args?.workspace === "string" ? args.workspace : process.cwd();
33518
+ if (args?.config !== void 0) {
33519
+ const validation = validateChangeImpactConfig(args.config, "inline");
33520
+ return { success: validation.valid, source: "inline", sourceType: "mesh_policy", ...validation };
33521
+ }
33522
+ const loaded = loadChangeImpactConfig(workspace);
33523
+ if (loaded.sourceType === "repo_file") {
33524
+ const validation = validateChangeImpactConfig(loaded.config, loaded.source);
33525
+ return { success: validation.valid, ...loaded, ...validation };
33526
+ }
33527
+ return {
33528
+ success: false,
33529
+ ...loaded,
33530
+ valid: false,
33531
+ errors: [loaded.error || "repo change-impact config unavailable"]
33532
+ };
33533
+ },
33534
+ suggest_mesh_change_impact_config: async (_ctx, args) => {
33535
+ const workspace = typeof args?.workspace === "string" ? args.workspace : process.cwd();
33536
+ return {
33537
+ success: true,
33538
+ ...suggestChangeImpactConfig(workspace),
33539
+ note: "Suggestions are heuristic scaffold only; the draft must be reviewed and saved into repo change-impact config before it takes effect. Nothing is executed."
33540
+ };
33541
+ }
33542
+ };
33543
+
33544
+ // src/commands/low-family/index.ts
33545
+ var lowFamilyRegistry = new Map(
33546
+ Object.entries({
33547
+ ...sessionHostHandlers,
33548
+ ...specProviderDevHandlers,
33549
+ ...refineConfigHandlers
33550
+ })
33551
+ );
33552
+
33056
33553
  // src/commands/cli-manager.ts
33057
33554
  var os20 = __toESM(require("os"));
33058
33555
  var path26 = __toESM(require("path"));
@@ -36588,7 +37085,13 @@ var CliProviderInstance = class _CliProviderInstance {
36588
37085
  this.settings = {
36589
37086
  ...this.settings,
36590
37087
  meshNodeFor: assignment.meshId,
36591
- ...assignment.nodeId ? { meshNodeId: assignment.nodeId } : {},
37088
+ // WTCLAIM (A): track the bound node id under BOTH the active marker
37089
+ // (meshNodeId, cleared on detach) and a sticky marker (meshLastNodeId,
37090
+ // preserved across detach). The sticky marker lets a detached but still
37091
+ // coordinator-owned session be re-picked ONLY for the SAME node it served
37092
+ // — never auto-adopted for a sibling node (e.g. a cloned worktree) that
37093
+ // shares this daemon. See isMeshOwnedDelegateSession's post-detach gate.
37094
+ ...assignment.nodeId ? { meshNodeId: assignment.nodeId, meshLastNodeId: assignment.nodeId } : {},
36592
37095
  ...assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {},
36593
37096
  ...assignment.coordinatorDaemonId ? { meshCoordinatorDaemonId: assignment.coordinatorDaemonId } : {},
36594
37097
  // Session-level routing anchor: the originating coordinator session, so this
@@ -36607,9 +37110,9 @@ var CliProviderInstance = class _CliProviderInstance {
36607
37110
  if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
36608
37111
  const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
36609
37112
  void meshNodeFor;
36610
- void meshNodeId;
36611
37113
  void meshActiveTaskId;
36612
- this.settings = rest;
37114
+ const lastNodeId = typeof meshNodeId === "string" && meshNodeId.trim() ? meshNodeId.trim() : typeof rest.meshLastNodeId === "string" && rest.meshLastNodeId.trim() ? rest.meshLastNodeId.trim() : void 0;
37115
+ this.settings = lastNodeId ? { ...rest, meshLastNodeId: lastNodeId } : rest;
36613
37116
  this.adapter.updateRuntimeSettings?.(this.settings);
36614
37117
  }
36615
37118
  /**
@@ -39280,6 +39783,10 @@ function hasCompletedStartingLaunch(adapter) {
39280
39783
  if (hasNonEmptyModalButtons2(adapterStatus?.activeModal ?? adapterStatus?.modal ?? parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
39281
39784
  return hasFinalAssistantMessage(parsedStatus?.messages);
39282
39785
  }
39786
+ function normalizeDirForCompare(dir) {
39787
+ if (typeof dir !== "string") return "";
39788
+ return dir.trim().replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
39789
+ }
39283
39790
  function shouldSuppressStaleParsedBusyStatus(adapterStatus, parsedStatus, adapter) {
39284
39791
  const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
39285
39792
  if (!BUSY_AGENT_STATUSES.has(parsedRawStatus)) return false;
@@ -40028,6 +40535,33 @@ Run 'adhdev doctor' for detailed diagnostics.`
40028
40535
  const adapter = this.adapters.get(ik);
40029
40536
  return adapter ? { adapter, key: ik } : null;
40030
40537
  }
40538
+ /**
40539
+ * WTCLAIM (B): resolve the adapter for a mesh dispatch that named a node
40540
+ * (meshContext.nodeId) but carried no explicit session. Matches by the
40541
+ * instance's bound mesh node id (settings.meshNodeId, falling back to the
40542
+ * sticky meshLastNodeId) first, then by the node workspace (workingDir).
40543
+ *
40544
+ * Unlike findAdapter's step-2 fuzzy fallback, this NEVER degrades to a
40545
+ * provider-only first-match: on a daemon hosting BOTH a base node and a
40546
+ * cloned worktree node (same daemonId), that fuzzy match could land a
40547
+ * worktree-targeted task on the base session. Returns null when no session
40548
+ * is bound to this node so the caller fails closed.
40549
+ */
40550
+ findMeshNodeAdapter(agentType, nodeId, dir) {
40551
+ const instanceManager = this.deps.getInstanceManager();
40552
+ const targetDir = normalizeDirForCompare(dir);
40553
+ let workspaceMatch = null;
40554
+ for (const [k, a] of this.adapters) {
40555
+ if (a.cliType !== agentType) continue;
40556
+ const settings = instanceManager?.getInstance(k)?.getState?.()?.settings;
40557
+ const boundNodeId = typeof settings?.meshNodeId === "string" && settings.meshNodeId.trim() ? settings.meshNodeId.trim() : typeof settings?.meshLastNodeId === "string" ? settings.meshLastNodeId.trim() : "";
40558
+ if (boundNodeId && boundNodeId === nodeId) return { adapter: a, key: k };
40559
+ if (!workspaceMatch && targetDir && normalizeDirForCompare(a.workingDir) === targetDir) {
40560
+ workspaceMatch = { adapter: a, key: k };
40561
+ }
40562
+ }
40563
+ return workspaceMatch;
40564
+ }
40031
40565
  // ─── CLI command handling ────────────────────────────
40032
40566
  async handleCliCommand(cmd, args) {
40033
40567
  switch (cmd) {
@@ -40213,10 +40747,22 @@ Run 'adhdev doctor' for detailed diagnostics.`
40213
40747
  const agentType = args?.agentType || args?.cliType;
40214
40748
  const action = args?.action;
40215
40749
  if (!agentType || !action) throw new Error("agentType and action required");
40216
- const found = this.findAdapter(agentType, {
40217
- dir: args?.dir,
40218
- instanceKey: args?.targetSessionId
40219
- });
40750
+ const meshScopeNodeId = (() => {
40751
+ const mc = args?.meshContext;
40752
+ return mc && typeof mc === "object" && typeof mc.nodeId === "string" ? mc.nodeId.trim() : "";
40753
+ })();
40754
+ let found;
40755
+ if (meshScopeNodeId && !args?.targetSessionId) {
40756
+ found = this.findMeshNodeAdapter(agentType, meshScopeNodeId, args?.dir);
40757
+ if (!found) {
40758
+ throw new Error(`No mesh worker session bound to node '${meshScopeNodeId}' for agent '${agentType}' on this daemon; refusing provider-only fuzzy match to avoid cross-node dispatch`);
40759
+ }
40760
+ } else {
40761
+ found = this.findAdapter(agentType, {
40762
+ dir: args?.dir,
40763
+ instanceKey: args?.targetSessionId
40764
+ });
40765
+ }
40220
40766
  if (!found) throw new Error(`CLI agent not running: ${agentType}`);
40221
40767
  const { adapter, key } = found;
40222
40768
  if (action === "send_chat") {
@@ -44110,7 +44656,6 @@ init_workspaces();
44110
44656
  init_recent_activity();
44111
44657
  init_cli_detector();
44112
44658
  init_git_status();
44113
- init_change_impact_config();
44114
44659
  init_dist();
44115
44660
  init_logger();
44116
44661
 
@@ -45453,7 +45998,7 @@ function buildMeshNodeMachineIdentity(node, opts) {
45453
45998
  const machineName = readMeshNodeDisplayMachineName(node);
45454
45999
  const coordinatorHostname = readStringValue(opts.coordinatorHostname);
45455
46000
  const machineIdMatches = Boolean(opts.localMachineId && machineId && opts.localMachineId === machineId);
45456
- const daemonIdMatches = Boolean(opts.localDaemonId && daemonId && opts.localDaemonId === daemonId);
46001
+ const daemonIdMatches = Boolean(opts.localDaemonId && daemonId && daemonIdsEquivalent(opts.localDaemonId, daemonId));
45457
46002
  const hostnameMatches = Boolean(
45458
46003
  normalizeMeshHostname(hostname3) && normalizeMeshHostname(coordinatorHostname) && normalizeMeshHostname(hostname3) === normalizeMeshHostname(coordinatorHostname)
45459
46004
  );
@@ -46168,10 +46713,10 @@ async function hydrateInlineMeshDirectTruth(args) {
46168
46713
  const isSelfNode = Boolean(
46169
46714
  nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId
46170
46715
  ) || Boolean(
46171
- daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId)
46716
+ daemonId && (daemonIdsEquivalent(daemonId, args.localMachineId) || daemonIdsEquivalent(daemonId, args.statusInstanceId))
46172
46717
  ) || Boolean(args.meshSource !== "local_config" && nodeIndex === 0);
46173
46718
  const isSelfDaemonNode = Boolean(
46174
- daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId)
46719
+ daemonId && (daemonIdsEquivalent(daemonId, args.localMachineId) || daemonIdsEquivalent(daemonId, args.statusInstanceId))
46175
46720
  );
46176
46721
  if ((isSelfNode || isSelfDaemonNode) && isDeadLocalWorktreeNode(node)) {
46177
46722
  deadNodeIds.push(nodeId);
@@ -47510,97 +48055,6 @@ function normalizeCommandArgsWithInteractionId(args) {
47510
48055
  }
47511
48056
  return base;
47512
48057
  }
47513
- function resolveSpecPathInProviders(specPath, fsm, pathm, osm) {
47514
- let rootReal;
47515
- try {
47516
- rootReal = fsm.realpathSync(pathm.join(osm.homedir(), ".adhdev", "providers"));
47517
- } catch (e) {
47518
- return { ok: false, error: `providers root unavailable: ${e.message}` };
47519
- }
47520
- const resolved = pathm.resolve(specPath);
47521
- const base = pathm.basename(resolved);
47522
- if (!/^[\w.-]+\.json$/.test(base)) {
47523
- return { ok: false, error: "refused: spec file must be a *.json basename" };
47524
- }
47525
- let parentReal;
47526
- try {
47527
- parentReal = fsm.realpathSync(pathm.dirname(resolved));
47528
- } catch (e) {
47529
- return { ok: false, error: `spec directory not found: ${e.message}` };
47530
- }
47531
- if (parentReal !== rootReal && !parentReal.startsWith(rootReal + pathm.sep)) {
47532
- return { ok: false, error: "refused: spec path must be under the providers root" };
47533
- }
47534
- const safe = pathm.join(parentReal, base);
47535
- try {
47536
- const st = fsm.lstatSync(safe);
47537
- if (st.isSymbolicLink()) return { ok: false, error: "refused: spec path is a symlink" };
47538
- } catch {
47539
- }
47540
- return { ok: true, path: safe };
47541
- }
47542
- function toHostedCliRuntimeDescriptor(record) {
47543
- if (!record || typeof record !== "object") return null;
47544
- const runtimeId = typeof record.sessionId === "string" ? record.sessionId : "";
47545
- const cliType = typeof record.providerType === "string" ? record.providerType : "";
47546
- const workspace = typeof record.workspace === "string" ? record.workspace : "";
47547
- if (!runtimeId || !cliType || !workspace) return null;
47548
- return {
47549
- runtimeId,
47550
- runtimeKey: typeof record.runtimeKey === "string" ? record.runtimeKey : void 0,
47551
- displayName: typeof record.displayName === "string" ? record.displayName : void 0,
47552
- workspaceLabel: typeof record.workspaceLabel === "string" ? record.workspaceLabel : void 0,
47553
- lifecycle: typeof record.lifecycle === "string" ? record.lifecycle : void 0,
47554
- recoveryState: typeof record.meta?.runtimeRecoveryState === "string" ? String(record.meta.runtimeRecoveryState) : null,
47555
- cliType,
47556
- workspace,
47557
- cliArgs: Array.isArray(record.meta?.cliArgs) ? record.meta.cliArgs : [],
47558
- providerSessionId: typeof record.meta?.providerSessionId === "string" ? String(record.meta.providerSessionId) : void 0
47559
- };
47560
- }
47561
- function getWriteConflictOwnerClientId(error) {
47562
- const message = typeof error === "string" ? error : error instanceof Error ? error.message : "";
47563
- const match = /^Write owned by\s+(.+)$/.exec(message.trim());
47564
- return match?.[1]?.trim() || void 0;
47565
- }
47566
- function summarizeSessionHostRecord(result) {
47567
- if (!result || typeof result !== "object") return {};
47568
- const record = result;
47569
- return {
47570
- runtimeKey: typeof record.runtimeKey === "string" ? record.runtimeKey : void 0,
47571
- lifecycle: typeof record.lifecycle === "string" ? record.lifecycle : void 0,
47572
- surfaceKind: getSessionHostSurfaceKind(record),
47573
- attachedClientCount: Array.isArray(record.attachedClients) ? record.attachedClients.length : void 0,
47574
- hasWriteOwner: !!record.writeOwner,
47575
- writeOwnerClientId: typeof record.writeOwner?.clientId === "string" ? record.writeOwner.clientId : void 0
47576
- };
47577
- }
47578
- function summarizeSessionHostRecords(result) {
47579
- const records = Array.isArray(result) ? result : [];
47580
- const groups = partitionSessionHostRecords(records);
47581
- return {
47582
- sessionCount: records.length,
47583
- liveRuntimeCount: groups.liveRuntimes.length,
47584
- recoverySnapshotCount: groups.recoverySnapshots.length,
47585
- inactiveRecordCount: groups.inactiveRecords.length
47586
- };
47587
- }
47588
- function summarizeSessionHostDiagnostics(result) {
47589
- const diagnostics = result && typeof result === "object" ? result : {};
47590
- const sessions = Array.isArray(diagnostics.sessions) ? diagnostics.sessions : [];
47591
- return {
47592
- runtimeCount: typeof diagnostics.runtimeCount === "number" ? diagnostics.runtimeCount : void 0,
47593
- ...summarizeSessionHostRecords(sessions)
47594
- };
47595
- }
47596
- function summarizeSessionHostPruneResult(result) {
47597
- const value = result && typeof result === "object" ? result : {};
47598
- return {
47599
- duplicateGroupCount: typeof value.duplicateGroupCount === "number" ? value.duplicateGroupCount : void 0,
47600
- prunedCount: Array.isArray(value.prunedSessionIds) ? value.prunedSessionIds.length : void 0,
47601
- keptCount: Array.isArray(value.keptSessionIds) ? value.keptSessionIds.length : void 0
47602
- };
47603
- }
47604
48058
  function normalizeStandaloneHostCommandUrl(hostAddress) {
47605
48059
  const raw = hostAddress.trim();
47606
48060
  if (!raw) throw new Error("hostAddress required");
@@ -48450,56 +48904,6 @@ var DaemonCommandRouter = class {
48450
48904
  ...errors.length ? { errors } : {}
48451
48905
  };
48452
48906
  }
48453
- async traceSessionHostAction(action, args, run, summarizeResult2) {
48454
- const interactionId = typeof args?._interactionId === "string" ? args._interactionId : void 0;
48455
- const sessionId = typeof args?.sessionId === "string" ? args.sessionId : void 0;
48456
- const requestedPayload = { action };
48457
- if (sessionId) requestedPayload.sessionId = sessionId;
48458
- if (typeof args?.clientId === "string") requestedPayload.clientId = args.clientId;
48459
- if (typeof args?.signal === "string") requestedPayload.signal = args.signal;
48460
- if (typeof args?.providerType === "string") requestedPayload.providerType = args.providerType;
48461
- if (typeof args?.workspace === "string") requestedPayload.workspace = args.workspace;
48462
- if (typeof args?.dryRun === "boolean") requestedPayload.dryRun = args.dryRun;
48463
- recordDebugTrace({
48464
- interactionId,
48465
- category: "session_host",
48466
- stage: "action_requested",
48467
- level: "info",
48468
- sessionId,
48469
- payload: requestedPayload
48470
- });
48471
- try {
48472
- const result = await run();
48473
- recordDebugTrace({
48474
- interactionId,
48475
- category: "session_host",
48476
- stage: "action_result",
48477
- level: "info",
48478
- sessionId,
48479
- payload: {
48480
- ...requestedPayload,
48481
- success: true,
48482
- ...summarizeResult2 ? summarizeResult2(result) : {}
48483
- }
48484
- });
48485
- return result;
48486
- } catch (error) {
48487
- recordDebugTrace({
48488
- interactionId,
48489
- category: "session_host",
48490
- stage: "action_failed",
48491
- level: "error",
48492
- sessionId,
48493
- payload: {
48494
- ...requestedPayload,
48495
- error: error?.message || String(error),
48496
- failureKind: getWriteConflictOwnerClientId(error) ? "write_conflict" : "request_failed",
48497
- conflictOwnerClientId: getWriteConflictOwnerClientId(error)
48498
- }
48499
- });
48500
- throw error;
48501
- }
48502
- }
48503
48907
  /**
48504
48908
  * Unified command routing.
48505
48909
  * Returns result for all commands:
@@ -49874,6 +50278,10 @@ ${hintLines.join("\n")}` : "",
49874
50278
  }
49875
50279
  }
49876
50280
  }
50281
+ const lowFamilyHandler = lowFamilyRegistry.get(cmd);
50282
+ if (lowFamilyHandler) {
50283
+ return await lowFamilyHandler({ deps: this.deps }, args);
50284
+ }
49877
50285
  switch (cmd) {
49878
50286
  // ─── CLI / ACP commands ───
49879
50287
  case "mesh_forward_event": {
@@ -50001,121 +50409,6 @@ ${hintLines.join("\n")}` : "",
50001
50409
  const trace = getRecentDebugTrace({ interactionId, category, limit: count }).filter((entry) => !sinceTs || entry.ts > sinceTs);
50002
50410
  return { success: true, trace, count: trace.length };
50003
50411
  }
50004
- case "session_host_get_diagnostics": {
50005
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50006
- const diagnostics = await this.traceSessionHostAction("session_host_get_diagnostics", args, () => this.deps.sessionHostControl.getDiagnostics({
50007
- includeSessions: args?.includeSessions !== false,
50008
- limit: Number(args?.limit) || void 0
50009
- }), (result) => ({
50010
- includeSessions: args?.includeSessions !== false,
50011
- limit: Number(args?.limit) || void 0,
50012
- ...summarizeSessionHostDiagnostics(result)
50013
- }));
50014
- return { success: true, diagnostics };
50015
- }
50016
- case "session_host_list_sessions": {
50017
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50018
- const sessions = await this.traceSessionHostAction("session_host_list_sessions", args, () => this.deps.sessionHostControl.listSessions(), (records) => summarizeSessionHostRecords(records));
50019
- return { success: true, sessions };
50020
- }
50021
- case "session_host_stop_session": {
50022
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50023
- const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
50024
- if (!sessionId) return { success: false, error: "sessionId required" };
50025
- const record = await this.traceSessionHostAction("session_host_stop_session", args, () => this.deps.sessionHostControl.stopSession(sessionId), (result) => summarizeSessionHostRecord(result));
50026
- return { success: true, record };
50027
- }
50028
- case "session_host_resume_session": {
50029
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50030
- const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
50031
- if (!sessionId) return { success: false, error: "sessionId required" };
50032
- const record = await this.traceSessionHostAction("session_host_resume_session", args, async () => {
50033
- const nextRecord = await this.deps.sessionHostControl.resumeSession(sessionId);
50034
- const hosted = toHostedCliRuntimeDescriptor(nextRecord);
50035
- if (hosted) {
50036
- await this.deps.cliManager.restoreHostedSessions([hosted]);
50037
- }
50038
- return nextRecord;
50039
- }, (result) => ({
50040
- ...summarizeSessionHostRecord(result),
50041
- restoredHostedSession: !!toHostedCliRuntimeDescriptor(result)
50042
- }));
50043
- return { success: true, record };
50044
- }
50045
- case "session_host_restart_session": {
50046
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50047
- const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
50048
- if (!sessionId) return { success: false, error: "sessionId required" };
50049
- const record = await this.traceSessionHostAction("session_host_restart_session", args, async () => {
50050
- const nextRecord = await this.deps.sessionHostControl.restartSession(sessionId);
50051
- const hosted = toHostedCliRuntimeDescriptor(nextRecord);
50052
- if (hosted) {
50053
- await this.deps.cliManager.restoreHostedSessions([hosted]);
50054
- }
50055
- return nextRecord;
50056
- }, (result) => ({
50057
- ...summarizeSessionHostRecord(result),
50058
- restoredHostedSession: !!toHostedCliRuntimeDescriptor(result)
50059
- }));
50060
- return { success: true, record };
50061
- }
50062
- case "session_host_send_signal": {
50063
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50064
- const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
50065
- const signal = typeof args?.signal === "string" ? args.signal : "";
50066
- if (!sessionId) return { success: false, error: "sessionId required" };
50067
- if (!signal) return { success: false, error: "signal required" };
50068
- const record = await this.traceSessionHostAction("session_host_send_signal", args, () => this.deps.sessionHostControl.sendSignal(sessionId, signal), (result) => summarizeSessionHostRecord(result));
50069
- return { success: true, record };
50070
- }
50071
- case "session_host_force_detach_client": {
50072
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50073
- const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
50074
- const clientId = typeof args?.clientId === "string" ? args.clientId : "";
50075
- if (!sessionId) return { success: false, error: "sessionId required" };
50076
- if (!clientId) return { success: false, error: "clientId required" };
50077
- const record = await this.traceSessionHostAction("session_host_force_detach_client", args, () => this.deps.sessionHostControl.forceDetachClient(sessionId, clientId), (result) => summarizeSessionHostRecord(result));
50078
- return { success: true, record };
50079
- }
50080
- case "session_host_prune_duplicate_sessions": {
50081
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50082
- const result = await this.traceSessionHostAction("session_host_prune_duplicate_sessions", args, () => this.deps.sessionHostControl.pruneDuplicateSessions({
50083
- providerType: typeof args?.providerType === "string" ? args.providerType : void 0,
50084
- workspace: typeof args?.workspace === "string" ? args.workspace : void 0,
50085
- dryRun: args?.dryRun === true
50086
- }), (value) => summarizeSessionHostPruneResult(value));
50087
- return { success: true, result };
50088
- }
50089
- case "session_host_acquire_write": {
50090
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50091
- const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
50092
- const clientId = typeof args?.clientId === "string" ? args.clientId : "";
50093
- const ownerType = args?.ownerType === "agent" ? "agent" : "user";
50094
- if (!sessionId) return { success: false, error: "sessionId required" };
50095
- if (!clientId) return { success: false, error: "clientId required" };
50096
- const record = await this.traceSessionHostAction("session_host_acquire_write", args, () => this.deps.sessionHostControl.acquireWrite({
50097
- sessionId,
50098
- clientId,
50099
- ownerType,
50100
- force: args?.force !== false
50101
- }), (result) => ({
50102
- ...summarizeSessionHostRecord(result),
50103
- ownerType
50104
- }));
50105
- return { success: true, record };
50106
- }
50107
- case "session_host_release_write": {
50108
- if (!this.deps.sessionHostControl) return { success: false, error: "Session host control unavailable" };
50109
- const sessionId = typeof args?.sessionId === "string" ? args.sessionId : "";
50110
- const clientId = typeof args?.clientId === "string" ? args.clientId : "";
50111
- if (!sessionId) return { success: false, error: "sessionId required" };
50112
- if (!clientId) return { success: false, error: "clientId required" };
50113
- const record = await this.traceSessionHostAction("session_host_release_write", args, () => this.deps.sessionHostControl.releaseWrite({
50114
- sessionId,
50115
- clientId
50116
- }), (result) => summarizeSessionHostRecord(result));
50117
- return { success: true, record };
50118
- }
50119
50412
  case "list_saved_sessions": {
50120
50413
  const providerType = typeof args?.providerType === "string" ? args.providerType.trim() : typeof args?.agentType === "string" ? args.agentType.trim() : "";
50121
50414
  const kind = args?.kind === "acp" ? "acp" : "cli";
@@ -50371,166 +50664,6 @@ ${hintLines.join("\n")}` : "",
50371
50664
  } : null
50372
50665
  };
50373
50666
  }
50374
- case "get_spec_debug": {
50375
- const sessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : typeof args?.sessionId === "string" ? args.sessionId.trim() : "";
50376
- if (!sessionId) return { success: false, error: "targetSessionId required" };
50377
- const target = this.deps.sessionRegistry.get(sessionId);
50378
- if (!target) return { success: false, error: "Session not found", sessionId };
50379
- const adapterObj = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
50380
- const snapshot = adapterObj ? typeof adapterObj.getDebugSnapshot === "function" ? adapterObj.getDebugSnapshot() : typeof adapterObj.getDebugState === "function" ? adapterObj.getDebugState() : null : null;
50381
- return {
50382
- success: true,
50383
- sessionId,
50384
- providerType: target.providerType,
50385
- isSpecProvider: snapshot !== null,
50386
- snapshot
50387
- };
50388
- }
50389
- // ── Spec source read/write for the debug panel's live editor.
50390
- // Lets the dashboard load a session's spec.json, edit it, and
50391
- // save it back — the driver's fs.watch picks up the change and
50392
- // hot-reloads the FSM with no restart. Writes are confined to
50393
- // files under ~/.adhdev/providers to avoid arbitrary fs access.
50394
- case "get_spec_source": {
50395
- const fsm = await import("fs");
50396
- const pathm = await import("path");
50397
- const osm = await import("os");
50398
- const sessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : typeof args?.sessionId === "string" ? args.sessionId.trim() : "";
50399
- let specPath = typeof args?.specPath === "string" ? args.specPath : "";
50400
- if (!specPath && sessionId) {
50401
- const target = this.deps.sessionRegistry.get(sessionId);
50402
- const adapterObj = target ? this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter : null;
50403
- const snap = adapterObj && typeof adapterObj.getDebugSnapshot === "function" ? adapterObj.getDebugSnapshot() : null;
50404
- specPath = snap?.specPath ?? "";
50405
- }
50406
- if (!specPath) return { success: false, error: "specPath or resolvable targetSessionId required" };
50407
- const safe = resolveSpecPathInProviders(specPath, fsm, pathm, osm);
50408
- if (!safe.ok) return { success: false, error: safe.error, specPath };
50409
- try {
50410
- const content = fsm.readFileSync(safe.path, "utf8");
50411
- return { success: true, specPath: safe.path, content };
50412
- } catch (e) {
50413
- return { success: false, error: `read failed: ${e.message}`, specPath };
50414
- }
50415
- }
50416
- case "write_spec_source": {
50417
- const fsm = await import("fs");
50418
- const pathm = await import("path");
50419
- const osm = await import("os");
50420
- const specPath = typeof args?.specPath === "string" ? args.specPath : "";
50421
- const content = typeof args?.content === "string" ? args.content : "";
50422
- if (!specPath) return { success: false, error: "specPath required" };
50423
- if (!content) return { success: false, error: "content required" };
50424
- const safe = resolveSpecPathInProviders(specPath, fsm, pathm, osm);
50425
- if (!safe.ok) return { success: false, error: safe.error };
50426
- let parsed;
50427
- try {
50428
- parsed = JSON.parse(content);
50429
- } catch (e) {
50430
- return { success: false, error: `invalid JSON: ${e.message}` };
50431
- }
50432
- if (parsed?.$schema === "adhdev:cli/spec@4") {
50433
- const { validateFsmSpec: validateFsmSpec2 } = await Promise.resolve().then(() => (init_fsm_loader(), fsm_loader_exports));
50434
- const errs = validateFsmSpec2(parsed);
50435
- if (errs.length) return { success: false, error: "spec invalid", validationErrors: errs };
50436
- }
50437
- try {
50438
- fsm.writeFileSync(safe.path, content, "utf8");
50439
- return { success: true, specPath: safe.path };
50440
- } catch (e) {
50441
- return { success: false, error: `write failed: ${e.message}` };
50442
- }
50443
- }
50444
- // ── Validate an in-progress spec (string or object) without writing.
50445
- // The form builder calls this on every change so Save can stay
50446
- // disabled while there are structural / reference / regex errors.
50447
- case "validate_spec": {
50448
- let parsed = args?.spec;
50449
- if (typeof args?.content === "string") {
50450
- try {
50451
- parsed = JSON.parse(args.content);
50452
- } catch (e) {
50453
- return { success: true, valid: false, errors: [`invalid JSON: ${e.message}`] };
50454
- }
50455
- }
50456
- if (!parsed || typeof parsed !== "object") {
50457
- return { success: true, valid: false, errors: ["spec must be an object or content string"] };
50458
- }
50459
- const schema = parsed.$schema;
50460
- if (schema === "adhdev:cli/spec@4") {
50461
- const { validateFsmSpec: validateFsmSpec2 } = await Promise.resolve().then(() => (init_fsm_loader(), fsm_loader_exports));
50462
- const errors = validateFsmSpec2(parsed);
50463
- return { success: true, valid: errors.length === 0, errors };
50464
- }
50465
- return { success: true, valid: false, errors: [`unsupported $schema "${schema}" \u2014 form builder is v4-only`] };
50466
- }
50467
- // ── Evaluate a single condition against a live session's current
50468
- // screen — powers the editor's "does this match right now?"
50469
- // preview. Returns the recursive match tree.
50470
- case "eval_condition_preview": {
50471
- const sessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : typeof args?.sessionId === "string" ? args.sessionId.trim() : "";
50472
- if (!sessionId) return { success: false, error: "targetSessionId required" };
50473
- if (!args?.condition || typeof args.condition !== "object") return { success: false, error: "condition required" };
50474
- const target = this.deps.sessionRegistry.get(sessionId);
50475
- if (!target) return { success: false, error: "Session not found", sessionId };
50476
- const adapterObj = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
50477
- const snap = adapterObj && typeof adapterObj.getDebugSnapshot === "function" ? adapterObj.getDebugSnapshot() : null;
50478
- if (!snap?.screen) return { success: false, error: "no live screen for session" };
50479
- let sectionsDef;
50480
- try {
50481
- const fsm2 = await import("fs");
50482
- if (snap.specPath) {
50483
- const raw = JSON.parse(fsm2.readFileSync(snap.specPath, "utf8"));
50484
- sectionsDef = raw?.sections;
50485
- }
50486
- } catch {
50487
- }
50488
- const { evaluateConditionPreview: evaluateConditionPreview2 } = await Promise.resolve().then(() => (init_fsm_evaluator(), fsm_evaluator_exports));
50489
- try {
50490
- const result = evaluateConditionPreview2(
50491
- args.condition,
50492
- sectionsDef,
50493
- snap.screen,
50494
- snap.cursorPosition ?? void 0
50495
- );
50496
- return { success: true, result, sections: snap.sections ?? null };
50497
- } catch (e) {
50498
- return { success: false, error: `eval failed: ${e.message}` };
50499
- }
50500
- }
50501
- // ── Resolve a sections map against a live session's screen — the
50502
- // section editor's "test" button. Returns, for each section id,
50503
- // the line range + the text it captures, so the author can SEE
50504
- // whether a from_top/until/anchor definition carves the screen
50505
- // the way they intend. Accepts an in-progress sections map so it
50506
- // previews unsaved edits.
50507
- case "resolve_section_preview": {
50508
- const sessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : typeof args?.sessionId === "string" ? args.sessionId.trim() : "";
50509
- if (!sessionId) return { success: false, error: "targetSessionId required" };
50510
- if (!args?.sections || typeof args.sections !== "object") return { success: false, error: "sections map required" };
50511
- const target = this.deps.sessionRegistry.get(sessionId);
50512
- if (!target) return { success: false, error: "Session not found", sessionId };
50513
- const adapterObj = this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
50514
- const snap = adapterObj && typeof adapterObj.getDebugSnapshot === "function" ? adapterObj.getDebugSnapshot() : null;
50515
- if (!snap?.screen) return { success: false, error: "no live screen for session" };
50516
- const { resolveSections: resolveSections2 } = await Promise.resolve().then(() => (init_evaluator(), evaluator_exports));
50517
- try {
50518
- const lines = String(snap.screen).split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
50519
- const resolved = resolveSections2(args.sections, lines);
50520
- return {
50521
- success: true,
50522
- screenLineCount: lines.length,
50523
- sections: resolved.map((s2) => ({ id: s2.id, fromLine: s2.fromLine, toLine: s2.toLine, text: s2.text }))
50524
- };
50525
- } catch (e) {
50526
- return { success: false, error: `resolve failed: ${e.message}` };
50527
- }
50528
- }
50529
- // ── User-level coordinator-prompt files (~/.adhdev/coordinator-prompts/).
50530
- // These live on this daemon's filesystem and never sync to the
50531
- // cloud / other daemons — they're per-machine config. The
50532
- // Settings page in the dashboard reads/writes via these two
50533
- // commands instead of going through fs from the browser.
50534
50667
  case "list_coordinator_prompts": {
50535
50668
  const fs32 = await import("fs");
50536
50669
  const path42 = await import("path");
@@ -51251,73 +51384,6 @@ ${hintLines.join("\n")}` : "",
51251
51384
  return { success: false, error: e.message };
51252
51385
  }
51253
51386
  }
51254
- case "get_mesh_refine_config_schema": {
51255
- return {
51256
- success: true,
51257
- schema: MESH_REFINE_CONFIG_SCHEMA,
51258
- locations: MESH_REFINE_CONFIG_LOCATIONS,
51259
- worktreeBootstrap: {
51260
- schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
51261
- locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
51262
- sourceOfTruth: "repo worktree bootstrap config",
51263
- runBehavior: "When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node."
51264
- },
51265
- sourceOfTruth: "repo mesh/refine config",
51266
- heuristicRole: "suggestions_only_not_execution_path"
51267
- };
51268
- }
51269
- case "validate_mesh_refine_config": {
51270
- const workspace = typeof args?.workspace === "string" ? args.workspace : process.cwd();
51271
- const mesh = args?.inlineMesh || {};
51272
- const loaded = args?.config !== void 0 ? { config: args.config, source: "inline", sourceType: "mesh_policy" } : loadMeshRefineConfig(mesh, workspace);
51273
- const validation = loaded.config ? validateMeshRefineConfig(loaded.config, loaded.source) : { valid: false, errors: [loaded.error || "repo mesh/refine config unavailable"], commands: [], rejectedCommands: [] };
51274
- return { success: validation.valid, ...loaded, ...validation };
51275
- }
51276
- case "suggest_mesh_refine_config": {
51277
- const workspace = typeof args?.workspace === "string" ? args.workspace : process.cwd();
51278
- const mesh = args?.inlineMesh || {};
51279
- return {
51280
- success: true,
51281
- ...suggestMeshRefineConfig(mesh, workspace),
51282
- note: "Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config."
51283
- };
51284
- }
51285
- case "get_mesh_change_impact_config_schema": {
51286
- return {
51287
- success: true,
51288
- schema: CHANGE_IMPACT_CONFIG_SCHEMA,
51289
- locations: CHANGE_IMPACT_CONFIG_LOCATIONS,
51290
- sourceOfTruth: "repo change-impact config",
51291
- heuristicRole: "suggestions_only_not_execution_path",
51292
- note: "Declarative config only \u2014 JSON/YAML are parsed but never executed. Defines which package/file changes require a daemon rebuild/restart vs. a web-only redeploy vs. nothing."
51293
- };
51294
- }
51295
- case "validate_mesh_change_impact_config": {
51296
- const workspace = typeof args?.workspace === "string" ? args.workspace : process.cwd();
51297
- if (args?.config !== void 0) {
51298
- const validation = validateChangeImpactConfig(args.config, "inline");
51299
- return { success: validation.valid, source: "inline", sourceType: "mesh_policy", ...validation };
51300
- }
51301
- const loaded = loadChangeImpactConfig(workspace);
51302
- if (loaded.sourceType === "repo_file") {
51303
- const validation = validateChangeImpactConfig(loaded.config, loaded.source);
51304
- return { success: validation.valid, ...loaded, ...validation };
51305
- }
51306
- return {
51307
- success: false,
51308
- ...loaded,
51309
- valid: false,
51310
- errors: [loaded.error || "repo change-impact config unavailable"]
51311
- };
51312
- }
51313
- case "suggest_mesh_change_impact_config": {
51314
- const workspace = typeof args?.workspace === "string" ? args.workspace : process.cwd();
51315
- return {
51316
- success: true,
51317
- ...suggestChangeImpactConfig(workspace),
51318
- note: "Suggestions are heuristic scaffold only; the draft must be reviewed and saved into repo change-impact config before it takes effect. Nothing is executed."
51319
- };
51320
- }
51321
51387
  case "mesh_init": {
51322
51388
  const workspace = typeof args?.workspace === "string" && args.workspace.trim() ? args.workspace.trim() : process.cwd();
51323
51389
  const mesh = args?.inlineMesh || {};
@@ -52520,7 +52586,7 @@ ${ptyResult.output.slice(-2e3)}`);
52520
52586
  const isSelfNode = Boolean(
52521
52587
  nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId
52522
52588
  ) || Boolean(
52523
- daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId)
52589
+ daemonId && (daemonIdsEquivalent(daemonId, localMachineId) || daemonIdsEquivalent(daemonId, this.deps.statusInstanceId))
52524
52590
  ) || Boolean(meshRecord?.inline && nodeIndex === 0) || sparseConfiguredCoordinatorNode;
52525
52591
  const machineIdentity = buildMeshNodeMachineIdentity(node, {
52526
52592
  localMachineId,