oh-my-copilot 4.13.21 → 4.13.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +8 -0
  4. package/agents/code-reviewer.agent.md +24 -6
  5. package/agents/designer.agent.md +12 -0
  6. package/bridge/cli.cjs +1978 -1214
  7. package/bridge/mcp-server.cjs +678 -261
  8. package/bridge/runtime-cli.cjs +55 -8
  9. package/bridge/team.js +507 -38
  10. package/dist/__tests__/agent-registry.test.js +92 -1
  11. package/dist/__tests__/agent-registry.test.js.map +1 -1
  12. package/dist/__tests__/bedrock-model-routing.test.js +18 -17
  13. package/dist/__tests__/bedrock-model-routing.test.js.map +1 -1
  14. package/dist/__tests__/delegation-enforcer.test.js +46 -6
  15. package/dist/__tests__/delegation-enforcer.test.js.map +1 -1
  16. package/dist/__tests__/hooks/session-end-cleanup.test.d.ts +2 -0
  17. package/dist/__tests__/hooks/session-end-cleanup.test.d.ts.map +1 -0
  18. package/dist/__tests__/hooks/session-end-cleanup.test.js +93 -0
  19. package/dist/__tests__/hooks/session-end-cleanup.test.js.map +1 -0
  20. package/dist/__tests__/hud/stdin.test.d.ts +2 -0
  21. package/dist/__tests__/hud/stdin.test.d.ts.map +1 -0
  22. package/dist/__tests__/hud/stdin.test.js +556 -0
  23. package/dist/__tests__/hud/stdin.test.js.map +1 -0
  24. package/dist/__tests__/model-routing-override-three-site-sync.test.d.ts +2 -0
  25. package/dist/__tests__/model-routing-override-three-site-sync.test.d.ts.map +1 -0
  26. package/dist/__tests__/model-routing-override-three-site-sync.test.js +75 -0
  27. package/dist/__tests__/model-routing-override-three-site-sync.test.js.map +1 -0
  28. package/dist/__tests__/non-claude-provider-detection.test.js +37 -0
  29. package/dist/__tests__/non-claude-provider-detection.test.js.map +1 -1
  30. package/dist/__tests__/package-dir-resolution-regression.test.js +25 -0
  31. package/dist/__tests__/package-dir-resolution-regression.test.js.map +1 -1
  32. package/dist/__tests__/pre-tool-enforcer.test.js +51 -0
  33. package/dist/__tests__/pre-tool-enforcer.test.js.map +1 -1
  34. package/dist/__tests__/session-start-script-context.test.js +121 -0
  35. package/dist/__tests__/session-start-script-context.test.js.map +1 -1
  36. package/dist/__tests__/setup-claude-md-script.test.js +44 -1
  37. package/dist/__tests__/setup-claude-md-script.test.js.map +1 -1
  38. package/dist/agents/definitions.d.ts.map +1 -1
  39. package/dist/agents/definitions.js +2 -1
  40. package/dist/agents/definitions.js.map +1 -1
  41. package/dist/autoresearch/__tests__/decide-outcome.test.d.ts +2 -0
  42. package/dist/autoresearch/__tests__/decide-outcome.test.d.ts.map +1 -0
  43. package/dist/autoresearch/__tests__/decide-outcome.test.js +69 -0
  44. package/dist/autoresearch/__tests__/decide-outcome.test.js.map +1 -0
  45. package/dist/autoresearch/runtime.d.ts.map +1 -1
  46. package/dist/autoresearch/runtime.js +16 -2
  47. package/dist/autoresearch/runtime.js.map +1 -1
  48. package/dist/cli/__tests__/launch.test.js +23 -0
  49. package/dist/cli/__tests__/launch.test.js.map +1 -1
  50. package/dist/cli/__tests__/self-improve-paths.test.js +3 -2
  51. package/dist/cli/__tests__/self-improve-paths.test.js.map +1 -1
  52. package/dist/cli/__tests__/team.test.js +100 -0
  53. package/dist/cli/__tests__/team.test.js.map +1 -1
  54. package/dist/cli/commands/__tests__/team-role-shorthand.test.js +30 -0
  55. package/dist/cli/commands/__tests__/team-role-shorthand.test.js.map +1 -1
  56. package/dist/cli/commands/__tests__/team.test.js +48 -1
  57. package/dist/cli/commands/__tests__/team.test.js.map +1 -1
  58. package/dist/cli/commands/team.d.ts +2 -0
  59. package/dist/cli/commands/team.d.ts.map +1 -1
  60. package/dist/cli/commands/team.js +9 -2
  61. package/dist/cli/commands/team.js.map +1 -1
  62. package/dist/cli/launch.d.ts.map +1 -1
  63. package/dist/cli/launch.js +7 -0
  64. package/dist/cli/launch.js.map +1 -1
  65. package/dist/cli/team.d.ts.map +1 -1
  66. package/dist/cli/team.js +34 -3
  67. package/dist/cli/team.js.map +1 -1
  68. package/dist/config/__tests__/loader.test.js +35 -4
  69. package/dist/config/__tests__/loader.test.js.map +1 -1
  70. package/dist/config/__tests__/models.test.js +126 -3
  71. package/dist/config/__tests__/models.test.js.map +1 -1
  72. package/dist/config/models.d.ts +8 -0
  73. package/dist/config/models.d.ts.map +1 -1
  74. package/dist/config/models.js +119 -8
  75. package/dist/config/models.js.map +1 -1
  76. package/dist/features/delegation-enforcer.d.ts.map +1 -1
  77. package/dist/features/delegation-enforcer.js +10 -9
  78. package/dist/features/delegation-enforcer.js.map +1 -1
  79. package/dist/hooks/__tests__/bridge-routing.test.js +1 -0
  80. package/dist/hooks/__tests__/bridge-routing.test.js.map +1 -1
  81. package/dist/hooks/autopilot/enforcement.d.ts.map +1 -1
  82. package/dist/hooks/autopilot/enforcement.js +19 -0
  83. package/dist/hooks/autopilot/enforcement.js.map +1 -1
  84. package/dist/hooks/bridge.d.ts.map +1 -1
  85. package/dist/hooks/bridge.js +26 -4
  86. package/dist/hooks/bridge.js.map +1 -1
  87. package/dist/hooks/persistent-mode/stop-hook-blocking.test.js +37 -1
  88. package/dist/hooks/persistent-mode/stop-hook-blocking.test.js.map +1 -1
  89. package/dist/hooks/session-end/index.d.ts +9 -2
  90. package/dist/hooks/session-end/index.d.ts.map +1 -1
  91. package/dist/hooks/session-end/index.js +90 -35
  92. package/dist/hooks/session-end/index.js.map +1 -1
  93. package/dist/hud/stdin.d.ts +12 -1
  94. package/dist/hud/stdin.d.ts.map +1 -1
  95. package/dist/hud/stdin.js +123 -18
  96. package/dist/hud/stdin.js.map +1 -1
  97. package/dist/hud/types.d.ts +1 -0
  98. package/dist/hud/types.d.ts.map +1 -1
  99. package/dist/hud/types.js.map +1 -1
  100. package/dist/installer/__tests__/session-start-template.test.d.ts +2 -0
  101. package/dist/installer/__tests__/session-start-template.test.d.ts.map +1 -0
  102. package/dist/installer/__tests__/session-start-template.test.js +174 -0
  103. package/dist/installer/__tests__/session-start-template.test.js.map +1 -0
  104. package/dist/lib/__tests__/mode-state-io.test.js +75 -12
  105. package/dist/lib/__tests__/mode-state-io.test.js.map +1 -1
  106. package/dist/lib/mode-state-io.d.ts +11 -0
  107. package/dist/lib/mode-state-io.d.ts.map +1 -1
  108. package/dist/lib/mode-state-io.js +64 -0
  109. package/dist/lib/mode-state-io.js.map +1 -1
  110. package/dist/lib/worktree-paths.d.ts +12 -0
  111. package/dist/lib/worktree-paths.d.ts.map +1 -1
  112. package/dist/lib/worktree-paths.js +75 -0
  113. package/dist/lib/worktree-paths.js.map +1 -1
  114. package/dist/planning/__tests__/artifacts.test.d.ts +2 -0
  115. package/dist/planning/__tests__/artifacts.test.d.ts.map +1 -0
  116. package/dist/planning/__tests__/artifacts.test.js +486 -0
  117. package/dist/planning/__tests__/artifacts.test.js.map +1 -0
  118. package/dist/planning/artifact-names.d.ts +13 -0
  119. package/dist/planning/artifact-names.d.ts.map +1 -0
  120. package/dist/planning/artifact-names.js +108 -0
  121. package/dist/planning/artifact-names.js.map +1 -0
  122. package/dist/planning/artifacts.d.ts +47 -0
  123. package/dist/planning/artifacts.d.ts.map +1 -0
  124. package/dist/planning/artifacts.js +221 -0
  125. package/dist/planning/artifacts.js.map +1 -0
  126. package/dist/team/__tests__/api-interop.compatibility.test.js +194 -5
  127. package/dist/team/__tests__/api-interop.compatibility.test.js.map +1 -1
  128. package/dist/team/__tests__/model-contract.test.js +92 -2
  129. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  130. package/dist/team/__tests__/runtime-v2.dispatch.test.js +30 -0
  131. package/dist/team/__tests__/runtime-v2.dispatch.test.js.map +1 -1
  132. package/dist/team/__tests__/scaling.test.js +39 -1
  133. package/dist/team/__tests__/scaling.test.js.map +1 -1
  134. package/dist/team/__tests__/worker-bootstrap.test.js +6 -0
  135. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  136. package/dist/team/api-interop.d.ts.map +1 -1
  137. package/dist/team/api-interop.js +89 -2
  138. package/dist/team/api-interop.js.map +1 -1
  139. package/dist/team/delegation-evidence.d.ts +5 -0
  140. package/dist/team/delegation-evidence.d.ts.map +1 -0
  141. package/dist/team/delegation-evidence.js +29 -0
  142. package/dist/team/delegation-evidence.js.map +1 -0
  143. package/dist/team/model-contract.d.ts +8 -0
  144. package/dist/team/model-contract.d.ts.map +1 -1
  145. package/dist/team/model-contract.js +13 -0
  146. package/dist/team/model-contract.js.map +1 -1
  147. package/dist/team/runtime-v2.d.ts +2 -1
  148. package/dist/team/runtime-v2.d.ts.map +1 -1
  149. package/dist/team/runtime-v2.js +3 -1
  150. package/dist/team/runtime-v2.js.map +1 -1
  151. package/dist/team/state/tasks.d.ts +4 -1
  152. package/dist/team/state/tasks.d.ts.map +1 -1
  153. package/dist/team/state/tasks.js +38 -1
  154. package/dist/team/state/tasks.js.map +1 -1
  155. package/dist/team/team-ops.d.ts +4 -1
  156. package/dist/team/team-ops.d.ts.map +1 -1
  157. package/dist/team/team-ops.js +2 -2
  158. package/dist/team/team-ops.js.map +1 -1
  159. package/dist/team/types.d.ts +22 -1
  160. package/dist/team/types.d.ts.map +1 -1
  161. package/dist/team/types.js.map +1 -1
  162. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  163. package/dist/team/worker-bootstrap.js +6 -1
  164. package/dist/team/worker-bootstrap.js.map +1 -1
  165. package/dist/tools/__tests__/state-tools.test.js +322 -5
  166. package/dist/tools/__tests__/state-tools.test.js.map +1 -1
  167. package/dist/tools/__tests__/wiki-tools-working-directory.test.d.ts +2 -0
  168. package/dist/tools/__tests__/wiki-tools-working-directory.test.d.ts.map +1 -0
  169. package/dist/tools/__tests__/wiki-tools-working-directory.test.js +66 -0
  170. package/dist/tools/__tests__/wiki-tools-working-directory.test.js.map +1 -0
  171. package/dist/tools/state-tools.d.ts +3 -10
  172. package/dist/tools/state-tools.d.ts.map +1 -1
  173. package/dist/tools/state-tools.js +393 -91
  174. package/dist/tools/state-tools.js.map +1 -1
  175. package/dist/tools/wiki-tools.js +8 -8
  176. package/dist/tools/wiki-tools.js.map +1 -1
  177. package/docs/GETTING-STARTED.md +7 -0
  178. package/docs/copilot-instructions.md +1 -1
  179. package/package.json +1 -1
  180. package/scripts/lib/model-routing-override-message.mjs +21 -0
  181. package/scripts/persistent-mode.cjs +36 -0
  182. package/scripts/persistent-mode.mjs +36 -0
  183. package/scripts/post-tool-verifier.mjs +1 -0
  184. package/scripts/pre-tool-enforcer.mjs +60 -0
  185. package/scripts/session-start.mjs +63 -0
  186. package/scripts/setup-copilot-instructions.sh +12 -2
  187. package/skills/configure-notifications/SKILL.md +3 -3
  188. package/skills/mcp-setup/SKILL.md +60 -45
  189. package/skills/omc-setup/SKILL.md +3 -3
  190. package/skills/omc-setup/phases/01-install-claude-md.md +1 -1
  191. package/skills/omc-setup/phases/04-welcome.md +8 -8
  192. package/skills/project-session-manager/SKILL.md +2 -2
  193. package/skills/project-session-manager/psm.sh +1 -1
  194. package/templates/hooks/lib/model-routing-override-message.mjs +21 -0
  195. package/templates/hooks/session-start.mjs +62 -0
@@ -5,10 +5,12 @@
5
5
  * All paths are validated to stay within the worktree boundary.
6
6
  */
7
7
  import { z } from 'zod';
8
- import { existsSync, readFileSync, unlinkSync } from 'fs';
9
- import { resolveStatePath, ensureOmcDir, validateWorkingDirectory, resolveSessionStatePath, ensureSessionStateDir, listSessionIds, validateSessionId, } from '../lib/worktree-paths.js';
8
+ import { existsSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { resolveStatePath, ensureOmcDir, validateWorkingDirectory, resolveSessionStatePath, ensureSessionStateDir, listSessionIds, validateSessionId, getOmcRoot, } from '../lib/worktree-paths.js';
10
11
  import { atomicWriteJsonSync } from '../lib/atomic-write.js';
11
12
  import { validatePayload } from '../lib/payload-limits.js';
13
+ import { canClearStateForSession, findCompletedSessionStateFiles, findSessionOwnedStateFiles, } from '../lib/mode-state-io.js';
12
14
  import { isModeActive, getActiveModes, getAllModeStatuses, clearModeState, getStateFilePath, MODE_CONFIGS, getActiveSessionsForMode } from '../hooks/mode-registry/index.js';
13
15
  // Canonical execution modes from mode-registry (deep-interview and self-improve
14
16
  // are first-class modes with dedicated MODE_CONFIGS entries; ralplan remains an
@@ -25,6 +27,86 @@ const STATE_TOOL_MODES = [
25
27
  ];
26
28
  const EXTRA_STATE_ONLY_MODES = ['ralplan', 'omc-teams', 'skill-active'];
27
29
  const CANCEL_SIGNAL_TTL_MS = 30_000;
30
+ function readTeamNamesFromStateFile(statePath) {
31
+ if (!existsSync(statePath))
32
+ return [];
33
+ try {
34
+ const raw = JSON.parse(readFileSync(statePath, 'utf-8'));
35
+ const teamName = typeof raw.team_name === 'string'
36
+ ? raw.team_name.trim()
37
+ : typeof raw.teamName === 'string'
38
+ ? raw.teamName.trim()
39
+ : '';
40
+ return teamName ? [teamName] : [];
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ }
46
+ function pruneMissionBoardTeams(root, teamNames) {
47
+ const missionStatePath = join(getOmcRoot(root), 'state', 'mission-state.json');
48
+ if (!existsSync(missionStatePath))
49
+ return 0;
50
+ try {
51
+ const parsed = JSON.parse(readFileSync(missionStatePath, 'utf-8'));
52
+ if (!Array.isArray(parsed.missions))
53
+ return 0;
54
+ const shouldRemoveAll = teamNames == null;
55
+ const teamNameSet = new Set(teamNames ?? []);
56
+ const remainingMissions = parsed.missions.filter((mission) => {
57
+ if (mission.source !== 'team')
58
+ return true;
59
+ if (shouldRemoveAll)
60
+ return false;
61
+ const missionTeamName = typeof mission.teamName === 'string'
62
+ ? mission.teamName.trim()
63
+ : typeof mission.name === 'string'
64
+ ? mission.name.trim()
65
+ : '';
66
+ return !missionTeamName || !teamNameSet.has(missionTeamName);
67
+ });
68
+ const removed = parsed.missions.length - remainingMissions.length;
69
+ if (removed > 0) {
70
+ writeFileSync(missionStatePath, JSON.stringify({
71
+ ...parsed,
72
+ updatedAt: new Date().toISOString(),
73
+ missions: remainingMissions,
74
+ }, null, 2));
75
+ }
76
+ return removed;
77
+ }
78
+ catch {
79
+ return 0;
80
+ }
81
+ }
82
+ function cleanupTeamRuntimeState(root, teamNames) {
83
+ const teamStateRoot = join(getOmcRoot(root), 'state', 'team');
84
+ if (!existsSync(teamStateRoot))
85
+ return 0;
86
+ const shouldRemoveAll = teamNames == null;
87
+ let removed = 0;
88
+ if (shouldRemoveAll) {
89
+ try {
90
+ rmSync(teamStateRoot, { recursive: true, force: true });
91
+ return 1;
92
+ }
93
+ catch {
94
+ return 0;
95
+ }
96
+ }
97
+ for (const teamName of teamNames ?? []) {
98
+ if (!teamName)
99
+ continue;
100
+ try {
101
+ rmSync(join(teamStateRoot, teamName), { recursive: true, force: true });
102
+ removed += 1;
103
+ }
104
+ catch {
105
+ // best effort
106
+ }
107
+ }
108
+ return removed;
109
+ }
28
110
  /**
29
111
  * Get the state file path for any mode (including swarm and ralplan).
30
112
  *
@@ -40,6 +122,104 @@ function getStatePath(mode, root) {
40
122
  // Fallback for modes not in registry (e.g., ralplan)
41
123
  return resolveStatePath(mode, root);
42
124
  }
125
+ function getLegacyStateFileCandidates(mode, root) {
126
+ const normalizedName = mode.endsWith('-state') ? mode : `${mode}-state`;
127
+ const candidates = [
128
+ getStatePath(mode, root),
129
+ join(getOmcRoot(root), `${normalizedName}.json`),
130
+ ];
131
+ return [...new Set(candidates)];
132
+ }
133
+ function clearLegacyStateCandidates(mode, root, sessionId) {
134
+ let cleared = 0;
135
+ let hadFailure = false;
136
+ for (const legacyPath of getLegacyStateFileCandidates(mode, root)) {
137
+ if (!existsSync(legacyPath)) {
138
+ continue;
139
+ }
140
+ try {
141
+ if (sessionId) {
142
+ const raw = JSON.parse(readFileSync(legacyPath, 'utf-8'));
143
+ if (!canClearStateForSession(raw, sessionId)) {
144
+ continue;
145
+ }
146
+ }
147
+ unlinkSync(legacyPath);
148
+ cleared++;
149
+ }
150
+ catch {
151
+ hadFailure = true;
152
+ }
153
+ }
154
+ return { cleared, hadFailure };
155
+ }
156
+ function clearSessionOwnedStateCandidates(mode, root, sessionId) {
157
+ let cleared = 0;
158
+ let hadFailure = false;
159
+ const paths = findSessionOwnedStateFiles(mode, sessionId, root);
160
+ for (const statePath of paths) {
161
+ try {
162
+ unlinkSync(statePath);
163
+ cleared++;
164
+ }
165
+ catch {
166
+ hadFailure = true;
167
+ }
168
+ }
169
+ return { cleared, hadFailure, paths };
170
+ }
171
+ function clearCompletedSessionStateCandidates(mode, root, requesterSessionId) {
172
+ let cleared = 0;
173
+ let hadFailure = false;
174
+ const paths = findCompletedSessionStateFiles(mode, root, requesterSessionId);
175
+ for (const statePath of paths) {
176
+ try {
177
+ unlinkSync(statePath);
178
+ cleared++;
179
+ }
180
+ catch {
181
+ hadFailure = true;
182
+ }
183
+ }
184
+ return { cleared, hadFailure, paths };
185
+ }
186
+ function getModeRuntimeArtifactNames(mode) {
187
+ return [
188
+ `${mode}-stop-breaker.json`,
189
+ `${mode}-last-steer-at`,
190
+ `${mode}-continue-steer.lock`,
191
+ ];
192
+ }
193
+ function clearModeRuntimeArtifacts(mode, root, sessionId) {
194
+ let cleared = 0;
195
+ let hadFailure = false;
196
+ const stateRoot = join(getOmcRoot(root), 'state');
197
+ const candidateDirs = new Set([stateRoot]);
198
+ if (sessionId) {
199
+ candidateDirs.add(join(stateRoot, 'sessions', sessionId));
200
+ }
201
+ else {
202
+ for (const sid of listSessionIds(root)) {
203
+ candidateDirs.add(join(stateRoot, 'sessions', sid));
204
+ }
205
+ }
206
+ for (const dir of candidateDirs) {
207
+ for (const artifactName of getModeRuntimeArtifactNames(mode)) {
208
+ const artifactPath = join(dir, artifactName);
209
+ if (!existsSync(artifactPath)) {
210
+ continue;
211
+ }
212
+ try {
213
+ unlinkSync(artifactPath);
214
+ cleared++;
215
+ }
216
+ catch {
217
+ hadFailure = true;
218
+ }
219
+ }
220
+ }
221
+ return { cleared, hadFailure };
222
+ }
43
223
  function writeSessionCancelSignal(root, sessionId, mode) {
44
224
  const now = Date.now();
45
225
  const cancelSignalPath = resolveSessionStatePath('cancel-signal', sessionId, root);
@@ -67,15 +247,6 @@ function isSessionModeActive(mode, root, sessionId) {
67
247
  return false;
68
248
  }
69
249
  }
70
- /**
71
- * Find a single other session that owns active state for the given mode.
72
- *
73
- * Resumed Ralph sessions can leave the only active state under a foreign
74
- * session directory. When the requester's session has no matching state,
75
- * this fallback lets `state_clear` reach the unambiguous single owner so
76
- * the stop-hook enforcement path clears cleanly. Returns undefined if there
77
- * are zero or multiple candidates — we never broad-clear across sessions.
78
- */
79
250
  function findSingleOwningSessionForMode(mode, root, requesterSessionId) {
80
251
  const owningSessions = listSessionIds(root).filter((sid) => (sid !== requesterSessionId && isSessionModeActive(mode, root, sid)));
81
252
  return owningSessions.length === 1 ? owningSessions[0] : undefined;
@@ -104,6 +275,26 @@ export const stateReadTool = {
104
275
  ? getStateFilePath(root, mode, sessionId)
105
276
  : resolveSessionStatePath(mode, sessionId, root);
106
277
  if (!existsSync(statePath)) {
278
+ const completedSessionPaths = findCompletedSessionStateFiles(mode, root, sessionId);
279
+ if (completedSessionPaths.length > 0) {
280
+ const orphanList = completedSessionPaths
281
+ .map((orphanPath) => {
282
+ const sessionMarker = `${join('state', 'sessions')}/`;
283
+ const markerIndex = orphanPath.indexOf(sessionMarker);
284
+ if (markerIndex === -1)
285
+ return `- ${orphanPath}`;
286
+ const rest = orphanPath.slice(markerIndex + sessionMarker.length);
287
+ const orphanSessionId = rest.split(/[\\/]/)[0] || 'unknown';
288
+ return `- session: ${orphanSessionId}\n path: ${orphanPath}`;
289
+ })
290
+ .join('\n');
291
+ return {
292
+ content: [{
293
+ type: 'text',
294
+ text: `No state found for mode: ${mode} in session: ${sessionId}\nExpected path: ${statePath}\n\nDiscovered ${completedSessionPaths.length} completed-session orphan state file${completedSessionPaths.length === 1 ? '' : 's'} for this mode:\n${orphanList}\n\nRun state_clear(mode="${mode}", session_id="${sessionId}") to clear the current session plus these completed-session orphan files.`
295
+ }]
296
+ };
297
+ }
107
298
  return {
108
299
  content: [{
109
300
  type: 'text',
@@ -314,56 +505,103 @@ export const stateClearTool = {
314
505
  session_id: z.string().optional().describe('Session ID for session-scoped state isolation. When provided, the tool operates only within that session. When omitted, the tool aggregates legacy state plus all session-scoped state (may include other sessions).'),
315
506
  },
316
507
  handler: async (args) => {
317
- const { mode, workingDirectory, session_id } = args;
508
+ const { workingDirectory, session_id } = args;
509
+ // Zod 4 widens z.enum(tuple) value types; cast back to the runtime-validated mode.
510
+ const mode = args.mode;
318
511
  try {
319
512
  const root = validateWorkingDirectory(workingDirectory);
320
513
  const sessionId = session_id;
514
+ const cleanedTeamNames = new Set();
515
+ const collectTeamNamesForCleanup = (statePath) => {
516
+ if (mode !== 'team')
517
+ return;
518
+ for (const teamName of readTeamNamesFromStateFile(statePath)) {
519
+ cleanedTeamNames.add(teamName);
520
+ }
521
+ };
321
522
  // If session_id provided, clear only session-specific state
322
523
  if (sessionId) {
323
524
  validateSessionId(sessionId);
525
+ const requestedSessionOwnedPaths = findSessionOwnedStateFiles(mode, sessionId, root);
526
+ for (const teamStatePath of findSessionOwnedStateFiles('team', sessionId, root)) {
527
+ collectTeamNamesForCleanup(teamStatePath);
528
+ }
529
+ if (mode === 'team') {
530
+ for (const teamStatePath of findCompletedSessionStateFiles('team', root, sessionId)) {
531
+ collectTeamNamesForCleanup(teamStatePath);
532
+ }
533
+ }
534
+ const completedSessionCleanup = clearCompletedSessionStateCandidates(mode, root, sessionId);
535
+ const runtimeCleanup = clearModeRuntimeArtifacts(mode, root, sessionId);
324
536
  writeSessionCancelSignal(root, sessionId, mode);
325
537
  if (MODE_CONFIGS[mode]) {
326
- const requesterStatePath = getStateFilePath(root, mode, sessionId);
327
- const hadRequesterState = existsSync(requesterStatePath);
328
538
  const success = clearModeState(mode, root, sessionId);
329
- // Ghost-legacy cleanup: after clearing session file, also remove
330
- // any legacy file at .omcp/state/{mode}-state.json if it belongs
331
- // to this session (matching _meta.sessionId) or has no _meta block.
332
- let ghostCleaned = false;
333
- try {
334
- const legacyPath = getStateFilePath(root, mode);
335
- if (existsSync(legacyPath)) {
336
- const raw = JSON.parse(readFileSync(legacyPath, 'utf-8'));
337
- const meta = raw._meta;
338
- if (!meta || meta.sessionId === sessionId) {
339
- unlinkSync(legacyPath);
340
- ghostCleaned = true;
341
- }
342
- }
343
- }
344
- catch {
345
- // Best-effort ghost cleanup — ignore errors
346
- }
347
- // Cross-session fallback: if the requester had no local state and
348
- // no ghost legacy file, but exactly one other session owns active
349
- // state for this mode (e.g. a resumed Ralph whose state lives
350
- // under the original session), clear that owner too. Keep bounded
351
- // to the unambiguous single-owner case to avoid broad-clearing.
539
+ const sessionCleanup = clearSessionOwnedStateCandidates(mode, root, sessionId);
540
+ const legacyCleanup = clearLegacyStateCandidates(mode, root, sessionId);
352
541
  let ownerSessionId;
353
- if (!hadRequesterState && !ghostCleaned) {
542
+ let ownerSessionCleanup = { cleared: 0, hadFailure: false, paths: [] };
543
+ let ownerLegacyCleanup = { cleared: 0, hadFailure: false };
544
+ if (requestedSessionOwnedPaths.length === 0 &&
545
+ completedSessionCleanup.cleared === 0 &&
546
+ sessionCleanup.cleared === 0 &&
547
+ legacyCleanup.cleared === 0) {
354
548
  ownerSessionId = findSingleOwningSessionForMode(mode, root, sessionId);
355
549
  if (ownerSessionId) {
550
+ if (mode === 'team') {
551
+ for (const teamStatePath of findSessionOwnedStateFiles('team', ownerSessionId, root)) {
552
+ collectTeamNamesForCleanup(teamStatePath);
553
+ }
554
+ }
356
555
  writeSessionCancelSignal(root, ownerSessionId, mode);
556
+ const ownerRuntimeCleanup = clearModeRuntimeArtifacts(mode, root, ownerSessionId);
557
+ runtimeCleanup.cleared += ownerRuntimeCleanup.cleared;
558
+ runtimeCleanup.hadFailure ||= ownerRuntimeCleanup.hadFailure;
357
559
  clearModeState(mode, root, ownerSessionId);
560
+ ownerSessionCleanup = clearSessionOwnedStateCandidates(mode, root, ownerSessionId);
561
+ ownerLegacyCleanup = clearLegacyStateCandidates(mode, root, ownerSessionId);
358
562
  }
359
563
  }
360
- const ghostNote = ghostCleaned ? ' (ghost legacy file also removed)' : '';
361
- const ownerNote = ownerSessionId ? ` (cleared owning session: ${ownerSessionId})` : '';
362
- if (success || ownerSessionId) {
564
+ const ghostNoteParts = [];
565
+ if (legacyCleanup.cleared > 0) {
566
+ ghostNoteParts.push('ghost legacy file also removed');
567
+ }
568
+ if (completedSessionCleanup.cleared > 0) {
569
+ ghostNoteParts.push(`removed ${completedSessionCleanup.cleared} completed-session orphan file${completedSessionCleanup.cleared === 1 ? '' : 's'}`);
570
+ }
571
+ if (sessionCleanup.cleared > 0) {
572
+ ghostNoteParts.push(`removed ${sessionCleanup.cleared} recovered session file${sessionCleanup.cleared === 1 ? '' : 's'}`);
573
+ }
574
+ if (runtimeCleanup.cleared > 0) {
575
+ ghostNoteParts.push(`removed ${runtimeCleanup.cleared} runtime artifact${runtimeCleanup.cleared === 1 ? '' : 's'}`);
576
+ }
577
+ if (ownerSessionId) {
578
+ ghostNoteParts.push(`cleared owning session: ${ownerSessionId}`);
579
+ }
580
+ const ghostNote = ghostNoteParts.length > 0 ? ` (${ghostNoteParts.join(', ')})` : '';
581
+ const runtimeCleanupNote = (() => {
582
+ if (mode !== 'team')
583
+ return '';
584
+ const teamNames = [...cleanedTeamNames];
585
+ const removedRoots = cleanupTeamRuntimeState(root, teamNames);
586
+ const prunedMissions = pruneMissionBoardTeams(root, teamNames);
587
+ const details = [];
588
+ if (removedRoots > 0)
589
+ details.push(`removed ${removedRoots} team runtime root(s)`);
590
+ if (prunedMissions > 0)
591
+ details.push(`pruned ${prunedMissions} HUD mission entry(ies)`);
592
+ return details.length > 0 ? ` (${details.join(', ')})` : '';
593
+ })();
594
+ if (success &&
595
+ !legacyCleanup.hadFailure &&
596
+ !sessionCleanup.hadFailure &&
597
+ !completedSessionCleanup.hadFailure &&
598
+ !ownerSessionCleanup.hadFailure &&
599
+ !ownerLegacyCleanup.hadFailure &&
600
+ !runtimeCleanup.hadFailure) {
363
601
  return {
364
602
  content: [{
365
603
  type: 'text',
366
- text: `Successfully cleared state for mode: ${mode} in session: ${sessionId}${ghostNote}${ownerNote}`
604
+ text: `Successfully cleared state for mode: ${mode} in session: ${sessionId}${ghostNote}${runtimeCleanupNote}`
367
605
  }]
368
606
  };
369
607
  }
@@ -371,65 +609,111 @@ export const stateClearTool = {
371
609
  return {
372
610
  content: [{
373
611
  type: 'text',
374
- text: `Warning: Some files could not be removed for mode: ${mode} in session: ${sessionId}${ghostNote}${ownerNote}`
612
+ text: `Warning: Some files could not be removed for mode: ${mode} in session: ${sessionId}${ghostNote}${runtimeCleanupNote}`
375
613
  }]
376
614
  };
377
615
  }
378
616
  }
379
617
  // Fallback for modes not in registry (e.g., ralplan)
380
- const statePath = resolveSessionStatePath(mode, sessionId, root);
381
- const hadRequesterState = existsSync(statePath);
382
- if (hadRequesterState) {
383
- unlinkSync(statePath);
384
- }
385
- // Ghost-legacy cleanup for non-registry modes
386
- let ghostCleaned = false;
387
- try {
388
- const legacyPath = resolveStatePath(mode, root);
389
- if (existsSync(legacyPath)) {
390
- const raw = JSON.parse(readFileSync(legacyPath, 'utf-8'));
391
- const meta = raw._meta;
392
- if (!meta || meta.sessionId === sessionId) {
393
- unlinkSync(legacyPath);
394
- ghostCleaned = true;
395
- }
396
- }
397
- }
398
- catch {
399
- // Best-effort ghost cleanup
400
- }
401
- // Same cross-session fallback as the registry branch above.
618
+ const sessionCleanup = clearSessionOwnedStateCandidates(mode, root, sessionId);
619
+ const legacyCleanup = clearLegacyStateCandidates(mode, root, sessionId);
402
620
  let ownerSessionId;
403
- if (!hadRequesterState && !ghostCleaned) {
621
+ let ownerSessionCleanup = { cleared: 0, hadFailure: false, paths: [] };
622
+ let ownerLegacyCleanup = { cleared: 0, hadFailure: false };
623
+ if (requestedSessionOwnedPaths.length === 0 &&
624
+ completedSessionCleanup.cleared === 0 &&
625
+ sessionCleanup.cleared === 0 &&
626
+ legacyCleanup.cleared === 0) {
404
627
  ownerSessionId = findSingleOwningSessionForMode(mode, root, sessionId);
405
628
  if (ownerSessionId) {
406
- writeSessionCancelSignal(root, ownerSessionId, mode);
407
- const ownerStatePath = resolveSessionStatePath(mode, ownerSessionId, root);
408
- if (existsSync(ownerStatePath)) {
409
- try {
410
- unlinkSync(ownerStatePath);
629
+ if (mode === 'team') {
630
+ for (const teamStatePath of findSessionOwnedStateFiles('team', ownerSessionId, root)) {
631
+ collectTeamNamesForCleanup(teamStatePath);
411
632
  }
412
- catch { /* best-effort */ }
413
633
  }
634
+ writeSessionCancelSignal(root, ownerSessionId, mode);
635
+ const ownerRuntimeCleanup = clearModeRuntimeArtifacts(mode, root, ownerSessionId);
636
+ runtimeCleanup.cleared += ownerRuntimeCleanup.cleared;
637
+ runtimeCleanup.hadFailure ||= ownerRuntimeCleanup.hadFailure;
638
+ ownerSessionCleanup = clearSessionOwnedStateCandidates(mode, root, ownerSessionId);
639
+ ownerLegacyCleanup = clearLegacyStateCandidates(mode, root, ownerSessionId);
414
640
  }
415
641
  }
416
- const ghostNote = ghostCleaned ? ' (ghost legacy file also removed)' : '';
417
- const ownerNote = ownerSessionId ? ` (cleared owning session: ${ownerSessionId})` : '';
642
+ const ghostNoteParts = [];
643
+ if (legacyCleanup.cleared > 0) {
644
+ ghostNoteParts.push('ghost legacy file also removed');
645
+ }
646
+ if (completedSessionCleanup.cleared > 0) {
647
+ ghostNoteParts.push(`removed ${completedSessionCleanup.cleared} completed-session orphan file${completedSessionCleanup.cleared === 1 ? '' : 's'}`);
648
+ }
649
+ if (sessionCleanup.cleared > 0) {
650
+ ghostNoteParts.push(`removed ${sessionCleanup.cleared} recovered session file${sessionCleanup.cleared === 1 ? '' : 's'}`);
651
+ }
652
+ if (runtimeCleanup.cleared > 0) {
653
+ ghostNoteParts.push(`removed ${runtimeCleanup.cleared} runtime artifact${runtimeCleanup.cleared === 1 ? '' : 's'}`);
654
+ }
655
+ if (ownerSessionId) {
656
+ ghostNoteParts.push(`cleared owning session: ${ownerSessionId}`);
657
+ }
658
+ const ghostNote = ghostNoteParts.length > 0 ? ` (${ghostNoteParts.join(', ')})` : '';
659
+ const runtimeCleanupNote = (() => {
660
+ if (mode !== 'team')
661
+ return '';
662
+ const teamNames = [...cleanedTeamNames];
663
+ const removedRoots = cleanupTeamRuntimeState(root, teamNames);
664
+ const prunedMissions = pruneMissionBoardTeams(root, teamNames);
665
+ const details = [];
666
+ if (removedRoots > 0)
667
+ details.push(`removed ${removedRoots} team runtime root(s)`);
668
+ if (prunedMissions > 0)
669
+ details.push(`pruned ${prunedMissions} HUD mission entry(ies)`);
670
+ return details.length > 0 ? ` (${details.join(', ')})` : '';
671
+ })();
418
672
  return {
419
673
  content: [{
420
674
  type: 'text',
421
- text: `Successfully cleared state for mode: ${mode} in session: ${sessionId}${ghostNote}${ownerNote}`
675
+ text: `${legacyCleanup.hadFailure || sessionCleanup.hadFailure || completedSessionCleanup.hadFailure || ownerSessionCleanup.hadFailure || ownerLegacyCleanup.hadFailure || runtimeCleanup.hadFailure ? 'Warning: Some files could not be removed' : 'Successfully cleared state'} for mode: ${mode} in session: ${sessionId}${ghostNote}${runtimeCleanupNote}`
422
676
  }]
423
677
  };
424
678
  }
425
679
  // No session_id: clear from all locations (legacy + all sessions)
680
+ // Write cancel signals FIRST (before deleting files) so the stop hook's
681
+ // isSessionCancelInProgress check sees the signal during the deletion window.
682
+ // Mirrors the session_id path at line ~403. (patch: fix missing cancel signal)
683
+ {
684
+ const now = Date.now();
685
+ const cancelSignalPayload = {
686
+ active: true,
687
+ requested_at: new Date(now).toISOString(),
688
+ expires_at: new Date(now + CANCEL_SIGNAL_TTL_MS).toISOString(),
689
+ mode,
690
+ source: 'state_clear',
691
+ };
692
+ // Write to legacy path (checked by stop hook fallback)
693
+ const legacySignalPath = join(getOmcRoot(root), 'state', 'cancel-signal-state.json');
694
+ try {
695
+ atomicWriteJsonSync(legacySignalPath, cancelSignalPayload);
696
+ }
697
+ catch { /* best-effort */ }
698
+ // Write to each session path (checked by stop hook primary check)
699
+ for (const sid of listSessionIds(root)) {
700
+ try {
701
+ const sessionSignalPath = resolveSessionStatePath('cancel-signal', sid, root);
702
+ atomicWriteJsonSync(sessionSignalPath, cancelSignalPayload);
703
+ }
704
+ catch { /* best-effort */ }
705
+ }
706
+ }
707
+ const runtimeCleanup = clearModeRuntimeArtifacts(mode, root);
426
708
  let clearedCount = 0;
427
709
  const errors = [];
710
+ if (mode === 'team') {
711
+ collectTeamNamesForCleanup(getStateFilePath(root, 'team'));
712
+ }
428
713
  // Clear legacy path
429
714
  if (MODE_CONFIGS[mode]) {
430
- // Only clear if state file exists - avoid false counts for missing files
431
- const legacyStatePath = getStateFilePath(root, mode);
432
- if (existsSync(legacyStatePath)) {
715
+ const primaryLegacyStatePath = getStateFilePath(root, mode);
716
+ if (existsSync(primaryLegacyStatePath)) {
433
717
  if (clearModeState(mode, root)) {
434
718
  clearedCount++;
435
719
  }
@@ -438,21 +722,21 @@ export const stateClearTool = {
438
722
  }
439
723
  }
440
724
  }
441
- else {
442
- const statePath = getStatePath(mode, root);
443
- if (existsSync(statePath)) {
444
- try {
445
- unlinkSync(statePath);
446
- clearedCount++;
447
- }
448
- catch {
449
- errors.push('legacy path');
450
- }
451
- }
725
+ const extraLegacyCleanup = clearLegacyStateCandidates(mode, root);
726
+ clearedCount += extraLegacyCleanup.cleared;
727
+ if (extraLegacyCleanup.hadFailure) {
728
+ errors.push('legacy path');
729
+ }
730
+ clearedCount += runtimeCleanup.cleared;
731
+ if (runtimeCleanup.hadFailure) {
732
+ errors.push('runtime artifacts');
452
733
  }
453
734
  // Clear all session-scoped state files
454
735
  const sessionIds = listSessionIds(root);
455
736
  for (const sid of sessionIds) {
737
+ if (mode === 'team') {
738
+ collectTeamNamesForCleanup(resolveSessionStatePath('team', sid, root));
739
+ }
456
740
  if (MODE_CONFIGS[mode]) {
457
741
  // Only clear if state file exists - avoid false counts for missing files
458
742
  const sessionStatePath = getStateFilePath(root, mode, sid);
@@ -478,7 +762,15 @@ export const stateClearTool = {
478
762
  }
479
763
  }
480
764
  }
481
- if (clearedCount === 0 && errors.length === 0) {
765
+ let removedTeamRoots = 0;
766
+ let prunedMissionEntries = 0;
767
+ if (mode === 'team') {
768
+ const teamNames = [...cleanedTeamNames];
769
+ const removeSelector = teamNames.length > 0 ? teamNames : undefined;
770
+ removedTeamRoots = cleanupTeamRuntimeState(root, removeSelector);
771
+ prunedMissionEntries = pruneMissionBoardTeams(root, removeSelector);
772
+ }
773
+ if (clearedCount === 0 && errors.length === 0 && removedTeamRoots === 0 && prunedMissionEntries === 0) {
482
774
  return {
483
775
  content: [{
484
776
  type: 'text',
@@ -490,6 +782,14 @@ export const stateClearTool = {
490
782
  if (errors.length > 0) {
491
783
  message += `\n- Errors: ${errors.join(', ')}`;
492
784
  }
785
+ if (mode === 'team') {
786
+ if (removedTeamRoots > 0) {
787
+ message += `\n- Team runtime roots removed: ${removedTeamRoots}`;
788
+ }
789
+ if (prunedMissionEntries > 0) {
790
+ message += `\n- HUD mission entries pruned: ${prunedMissionEntries}`;
791
+ }
792
+ }
493
793
  message += '\nWARNING: No session_id provided. Cleared legacy plus all session-scoped state; this is a broad operation that may affect other sessions.';
494
794
  return {
495
795
  content: [{
@@ -655,7 +955,9 @@ export const stateGetStatusTool = {
655
955
  session_id: z.string().optional().describe('Session ID for session-scoped state isolation. When provided, the tool operates only within that session. When omitted, the tool aggregates legacy state plus all session-scoped state (may include other sessions).'),
656
956
  },
657
957
  handler: async (args) => {
658
- const { mode, workingDirectory, session_id } = args;
958
+ const { workingDirectory, session_id } = args;
959
+ // Zod 4 widens z.enum(tuple) value types; cast back to the runtime-validated mode.
960
+ const mode = args.mode;
659
961
  try {
660
962
  const root = validateWorkingDirectory(workingDirectory);
661
963
  const sessionId = session_id;