sisyphi 1.1.32 → 1.1.34

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.
@@ -1,6 +1,7 @@
1
- - No static `hooks.json` — `src/daemon/agent.ts` generates it per-agent at spawn time. Script edits are invisible to running agents; **respawn required**.
2
- - `{type}-user-prompt.sh` naming does **not** auto-register. Must add to hardcoded `userPromptHooks` map in `src/daemon/agent.ts` or the script is never copied. Only `require-submit.sh`, `intercept-send-message.sh`, and `register-bg-task.sh` copy unconditionally. `userPromptHooks` only covers `UserPromptSubmit` — `plan-validate.sh` is the sole example of a PreToolUse hook, registered via a separate special-case block in `agent.ts`; new PreToolUse hooks for specific agent types need the same treatment.
3
- - `interactive: true` in agent frontmatter suppresses `require-submit.sh` from the Stop phase.
1
+ - The bundled `hooks.json` here is the **manifest** the daemon merges it with project (`.sisyphus/agent-plugin/hooks/hooks.json`) and user (`~/.sisyphus/agent-plugin/hooks/hooks.json`) layers at spawn time, then writes the merged result into the per-agent plugin dir. Script edits are invisible to running agents; **respawn required**.
2
+ - New hooks register declaratively: add a manifest entry with `agentTypes: [...]` (or `["all"]`) and the script gets copied automatically. No `src/daemon/agent.ts` edits needed for new agent-type-specific hooks. The merge logic lives in `src/daemon/extensions.ts` (`mergeHookManifests`, `collectReferencedHookScripts`).
3
+ - A bundled hook script is only copied into a spawned agent's plugin dir if the merged manifest references it for that agent type — scripts whose entries filter out (e.g. `plan-validate.sh` for a `review` agent) are skipped. Higher layers can suppress a bundled script by basename via `"disable": ["script.sh"]` at the manifest's top level.
4
+ - `interactive: true` in agent frontmatter triggers `condition: "non-interactive"` filtering — entries with that condition (currently the bundled `require-submit.sh` Stop hook) are dropped from the merged manifest for interactive agents.
4
5
  - Scripts receive no `{{placeholder}}` substitution — placeholders appear as literal text, unlike `.md` templates.
5
6
  - Prompt hooks (`userPrompt`, `systemPrompt`) write raw text to stdout. Pre-tool hooks (e.g. `intercept-send-message.sh`) write `{"decision":"block","reason":"..."}` or exit 0 — wrong format silently does nothing.
6
7
  - Claude Code invokes hooks unconditionally, not only in sisyphus sessions. Guard: `if [ -z "$SISYPHUS_SESSION_ID" ]; then exit 0; fi`. Stop hooks also need `$SISYPHUS_AGENT_ID`.
@@ -0,0 +1,51 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "SendMessage",
6
+ "agentTypes": ["all"],
7
+ "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/intercept-send-message.sh" }]
8
+ },
9
+ {
10
+ "matcher": "Bash",
11
+ "agentTypes": ["all"],
12
+ "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/ask-background-guard.sh" }]
13
+ },
14
+ {
15
+ "matcher": "Bash",
16
+ "agentTypes": ["plan"],
17
+ "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/plan-validate.sh" }]
18
+ },
19
+ {
20
+ "matcher": "Write|Edit|MultiEdit",
21
+ "agentTypes": ["plan"],
22
+ "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/plan-write-path.sh" }]
23
+ }
24
+ ],
25
+ "PostToolUse": [
26
+ {
27
+ "matcher": "Task",
28
+ "agentTypes": ["all"],
29
+ "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/register-bg-task.sh" }]
30
+ }
31
+ ],
32
+ "Stop": [
33
+ {
34
+ "agentTypes": ["all"],
35
+ "condition": "non-interactive",
36
+ "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/require-submit.sh" }]
37
+ }
38
+ ],
39
+ "UserPromptSubmit": [
40
+ { "agentTypes": ["problem"], "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/problem-user-prompt.sh" }] },
41
+ { "agentTypes": ["plan"], "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/plan-user-prompt.sh" }] },
42
+ { "agentTypes": ["spec"], "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/spec-user-prompt.sh" }] },
43
+ { "agentTypes": ["review"], "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/review-user-prompt.sh" }] },
44
+ { "agentTypes": ["review-plan"], "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/review-plan-user-prompt.sh" }] },
45
+ { "agentTypes": ["debug"], "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/debug-user-prompt.sh" }] },
46
+ { "agentTypes": ["operator"], "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/operator-user-prompt.sh" }] },
47
+ { "agentTypes": ["test-spec"], "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/test-spec-user-prompt.sh" }] },
48
+ { "agentTypes": ["explore"], "hooks": [{ "type": "command", "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/explore-user-prompt.sh" }] }
49
+ ]
50
+ }
51
+ }
package/dist/tui.js CHANGED
@@ -736,6 +736,35 @@ var init_atomic = __esm({
736
736
  }
737
737
  });
738
738
 
739
+ // src/shared/gitignore.ts
740
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
741
+ import { join as join8 } from "path";
742
+ var init_gitignore = __esm({
743
+ "src/shared/gitignore.ts"() {
744
+ "use strict";
745
+ }
746
+ });
747
+
748
+ // src/shared/types.ts
749
+ var init_types = __esm({
750
+ "src/shared/types.ts"() {
751
+ "use strict";
752
+ }
753
+ });
754
+
755
+ // src/daemon/state.ts
756
+ import { copyFileSync, cpSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
757
+ import { join as join9 } from "path";
758
+ var init_state = __esm({
759
+ "src/daemon/state.ts"() {
760
+ "use strict";
761
+ init_atomic();
762
+ init_paths();
763
+ init_gitignore();
764
+ init_types();
765
+ }
766
+ });
767
+
739
768
  // src/shared/shell.ts
740
769
  function shellQuote(s) {
741
770
  return `'${s.replace(/'/g, "'\\''")}'`;
@@ -748,8 +777,8 @@ var init_shell = __esm({
748
777
 
749
778
  // src/daemon/notify.ts
750
779
  import { spawn, execFile as execFile2 } from "child_process";
751
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync4 } from "fs";
752
- import { join as join8 } from "path";
780
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync6 } from "fs";
781
+ import { join as join10 } from "path";
753
782
  import { homedir as homedir3 } from "os";
754
783
  var TMUX_SOCKET, SWITCH_SCRIPT;
755
784
  var init_notify = __esm({
@@ -799,35 +828,6 @@ var init_notify = __esm({
799
828
  }
800
829
  });
801
830
 
802
- // src/shared/gitignore.ts
803
- import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
804
- import { join as join9 } from "path";
805
- var init_gitignore = __esm({
806
- "src/shared/gitignore.ts"() {
807
- "use strict";
808
- }
809
- });
810
-
811
- // src/shared/types.ts
812
- var init_types = __esm({
813
- "src/shared/types.ts"() {
814
- "use strict";
815
- }
816
- });
817
-
818
- // src/daemon/state.ts
819
- import { copyFileSync, cpSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
820
- import { join as join10 } from "path";
821
- var init_state = __esm({
822
- "src/daemon/state.ts"() {
823
- "use strict";
824
- init_atomic();
825
- init_paths();
826
- init_gitignore();
827
- init_types();
828
- }
829
- });
830
-
831
831
  // src/daemon/ask-store.ts
832
832
  import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
833
833
  function readDecisions(cwd2, sessionId2, askId2) {
@@ -878,6 +878,7 @@ var init_ask_store = __esm({
878
878
  init_paths();
879
879
  init_config();
880
880
  init_history();
881
+ init_state();
881
882
  init_atomic();
882
883
  init_notify();
883
884
  init_state();
@@ -4114,7 +4115,7 @@ function findLatestReport(cwd2, sessionId2) {
4114
4115
  return null;
4115
4116
  }
4116
4117
  }
4117
- function goToSessionWindow(state2, actions) {
4118
+ async function goToSessionWindow(state2, actions) {
4118
4119
  const session = state2.selectedSession;
4119
4120
  if (!session || !state2.selectedSessionId) {
4120
4121
  notify(state2, "No session selected");
@@ -4126,6 +4127,21 @@ function goToSessionWindow(state2, actions) {
4126
4127
  actions.selectWindow(session.tmuxWindowId);
4127
4128
  return;
4128
4129
  }
4130
+ if (session.status !== "completed") {
4131
+ const sessionId2 = state2.selectedSessionId;
4132
+ const res = await actions.send({ type: "reconnect", sessionId: sessionId2, cwd: state2.cwd });
4133
+ if (res.ok) {
4134
+ const data = res.data ?? {};
4135
+ const tmuxName = data["tmuxSessionName"] ?? session.tmuxSessionName;
4136
+ const tmuxWin = data["tmuxWindowId"];
4137
+ const tmuxId = data["tmuxSessionId"];
4138
+ const switchTarget = tmuxId ?? tmuxName;
4139
+ if (switchTarget) actions.switchToSession(switchTarget);
4140
+ if (tmuxWin) actions.selectWindow(tmuxWin);
4141
+ notify(state2, `Reconnected ${tmuxName ?? sessionId2}`);
4142
+ return;
4143
+ }
4144
+ }
4129
4145
  const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
4130
4146
  const claudeSessionId = lastCycle?.claudeSessionId;
4131
4147
  if (!claudeSessionId) {
@@ -4719,7 +4735,7 @@ function handleLeaderAction(action, state2, actions) {
4719
4735
  break;
4720
4736
  }
4721
4737
  case "go-to-window": {
4722
- goToSessionWindow(state2, actions);
4738
+ void goToSessionWindow(state2, actions);
4723
4739
  break;
4724
4740
  }
4725
4741
  case "clone-session": {
@@ -5088,7 +5104,7 @@ function handleNavigateKey(input, key, state2, actions) {
5088
5104
  return;
5089
5105
  }
5090
5106
  if (input === "w") {
5091
- goToSessionWindow(state2, actions);
5107
+ void goToSessionWindow(state2, actions);
5092
5108
  return;
5093
5109
  }
5094
5110
  if (input === "o") {
@@ -5277,6 +5293,35 @@ function handleNavigateKey(input, key, state2, actions) {
5277
5293
  requestRender();
5278
5294
  return;
5279
5295
  }
5296
+ if (input === "D") {
5297
+ const sessionId2 = state2.selectedSessionId;
5298
+ const session2 = state2.selectedSession;
5299
+ if (!sessionId2 || !session2) {
5300
+ notify(state2, "No session selected");
5301
+ return;
5302
+ }
5303
+ const next = !(session2.dangerousMode === true);
5304
+ session2.dangerousMode = next;
5305
+ requestRender();
5306
+ void (async () => {
5307
+ try {
5308
+ const res = await actions.send({ type: "set-dangerous-mode", sessionId: sessionId2, enabled: next });
5309
+ if (!res.ok) {
5310
+ if (state2.selectedSession === session2) session2.dangerousMode = !next;
5311
+ notify(state2, `Dangerous mode toggle failed: ${res.error}`);
5312
+ requestRender();
5313
+ return;
5314
+ }
5315
+ const flushed = res.data?.["flushed"] ?? 0;
5316
+ notify(state2, next ? flushed > 0 ? `DANGEROUS mode ON \u2014 ${flushed} pending ask(s) auto-resolved` : "DANGEROUS mode ON" : "DANGEROUS mode OFF");
5317
+ } catch (err) {
5318
+ if (state2.selectedSession === session2) session2.dangerousMode = !next;
5319
+ notify(state2, `Dangerous mode toggle failed: ${err.message}`);
5320
+ requestRender();
5321
+ }
5322
+ })();
5323
+ return;
5324
+ }
5280
5325
  if (input === "/") {
5281
5326
  state2.mode = "search";
5282
5327
  state2.searchText = "";
@@ -5470,17 +5515,30 @@ function listAllWindowIds() {
5470
5515
  return /* @__PURE__ */ new Set();
5471
5516
  }
5472
5517
  }
5473
- function registerDashboardWindow() {
5518
+ function registerDashboardWindow(cwd2) {
5474
5519
  const wid = getWindowId();
5475
5520
  const pane = process.env["TMUX_PANE"];
5521
+ let sessionTarget = null;
5476
5522
  if (pane) {
5477
- const session = execSafe(`tmux display-message -t ${shellQuote(pane)} -p "#{session_id}"`);
5478
- if (session) {
5479
- execSafe(`tmux set-option -t ${shellQuote(session)} @sisyphus_dashboard ${wid}`);
5480
- return;
5523
+ sessionTarget = execSafe(`tmux display-message -t ${shellQuote(pane)} -p "#{session_id}"`)?.trim() || null;
5524
+ }
5525
+ if (sessionTarget) {
5526
+ execSafe(`tmux set-option -t ${shellQuote(sessionTarget)} @sisyphus_dashboard ${wid}`);
5527
+ } else {
5528
+ execSafe(`tmux set-option @sisyphus_dashboard ${wid}`);
5529
+ }
5530
+ if (cwd2) {
5531
+ const normalizedCwd = cwd2.replace(/\/+$/, "");
5532
+ const target = sessionTarget;
5533
+ const existing = target ? execSafe(`tmux show-options -t ${shellQuote(target)} -v @sisyphus_cwd`)?.trim() : execSafe(`tmux show-options -v @sisyphus_cwd`)?.trim();
5534
+ if (!existing) {
5535
+ if (target) {
5536
+ execSafe(`tmux set-option -t ${shellQuote(target)} @sisyphus_cwd ${shellQuote(normalizedCwd)}`);
5537
+ } else {
5538
+ execSafe(`tmux set-option @sisyphus_cwd ${shellQuote(normalizedCwd)}`);
5539
+ }
5481
5540
  }
5482
5541
  }
5483
- execSafe(`tmux set-option @sisyphus_dashboard ${wid}`);
5484
5542
  }
5485
5543
  var companionPaneId = null;
5486
5544
  function setupCompanionPlugin() {
@@ -8073,6 +8131,7 @@ init_render();
8073
8131
  var B = ansiBold;
8074
8132
  var D = ansiDim;
8075
8133
  var SEP = D("\u2502 ");
8134
+ var DANGER_BADGE = "\x1B[1;41;97m DANGEROUS \x1B[0m";
8076
8135
  function renderStatusLine(buf, y, state2, cursorNodeType) {
8077
8136
  const { mode, focusPane, notification, error } = state2;
8078
8137
  if (mode === "report-detail") return;
@@ -8110,6 +8169,9 @@ function renderStatusLine(buf, y, state2, cursorNodeType) {
8110
8169
  }
8111
8170
  content = contextFilePart + B("[enter]") + D(" select ") + B("[m]") + D("essage ") + B("[n]") + D("ew ") + B("[w]") + D(" tmux ") + SEP + B("[q]") + D("uit");
8112
8171
  }
8172
+ if (state2.selectedSession?.dangerousMode === true) {
8173
+ content = `${DANGER_BADGE} ${content}`;
8174
+ }
8113
8175
  writeClipped(buf, 1, y, content, buf.width - 2);
8114
8176
  }
8115
8177
 
@@ -8797,7 +8859,7 @@ if (askId && sessionId) {
8797
8859
  await runSingleAsk2({ cwd, sessionId, askId });
8798
8860
  process.exit(0);
8799
8861
  }
8800
- registerDashboardWindow();
8862
+ registerDashboardWindow(cwd);
8801
8863
  var cleanup = setupTerminal();
8802
8864
  var state = createAppState(cwd);
8803
8865
  startApp(state, cleanup);