supipowers 2.0.2 → 2.2.0

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 (84) hide show
  1. package/README.md +5 -6
  2. package/package.json +4 -2
  3. package/skills/harness/SKILL.md +1 -0
  4. package/src/bootstrap.ts +8 -133
  5. package/src/commands/optimize-context.ts +153 -16
  6. package/src/commands/runbook.ts +511 -0
  7. package/src/config/defaults.ts +5 -5
  8. package/src/config/loader.ts +1 -0
  9. package/src/config/schema.ts +2 -6
  10. package/src/context/rule-renderer.ts +274 -2
  11. package/src/context/runbook-extension-template.ts +193 -0
  12. package/src/context/startup-check.ts +197 -2
  13. package/src/context/startup-optimizer.ts +133 -10
  14. package/src/context-mode/knowledge/store.ts +381 -43
  15. package/src/context-mode/tools.ts +41 -3
  16. package/src/deps/registry.ts +1 -12
  17. package/src/fix-pr/assessment.ts +1 -0
  18. package/src/fix-pr/prompt-builder.ts +1 -0
  19. package/src/git/commit.ts +76 -18
  20. package/src/harness/command.ts +201 -12
  21. package/src/harness/default-agents/docs.md +39 -0
  22. package/src/harness/docs/config.ts +29 -0
  23. package/src/harness/docs/glob-match.ts +27 -0
  24. package/src/harness/docs/index-renderer.ts +82 -0
  25. package/src/harness/docs/provenance.ts +125 -0
  26. package/src/harness/docs/regen-decision.ts +167 -0
  27. package/src/harness/docs/representative-files.ts +175 -0
  28. package/src/harness/docs/source-hash.ts +106 -0
  29. package/src/harness/docs/validator.ts +233 -0
  30. package/src/harness/git-verification.ts +515 -0
  31. package/src/harness/git-verify-qa.ts +406 -0
  32. package/src/harness/hooks/layer-context-inject.ts +35 -1
  33. package/src/harness/hooks/register.ts +24 -3
  34. package/src/harness/pipeline.ts +37 -13
  35. package/src/harness/pr-comment/baseline.ts +105 -0
  36. package/src/harness/pr-comment/ci-env.ts +120 -0
  37. package/src/harness/pr-comment/gh-poster.ts +227 -0
  38. package/src/harness/pr-comment/handler.ts +198 -0
  39. package/src/harness/pr-comment/render.ts +297 -0
  40. package/src/harness/pr-comment/status.ts +95 -0
  41. package/src/harness/pr-comment/types.ts +73 -0
  42. package/src/harness/pr-comment/workflow-summary.ts +47 -0
  43. package/src/harness/project-paths.ts +95 -0
  44. package/src/harness/stages/design.ts +1 -0
  45. package/src/harness/stages/discover.ts +1 -13
  46. package/src/harness/stages/docs.ts +708 -0
  47. package/src/harness/stages/implement-apply.ts +934 -0
  48. package/src/harness/stages/implement.ts +64 -51
  49. package/src/harness/stages/plan.ts +25 -16
  50. package/src/harness/stages/validate.ts +478 -0
  51. package/src/harness/storage.ts +142 -0
  52. package/src/harness/tools.ts +130 -0
  53. package/src/mempalace/bridge.ts +207 -41
  54. package/src/mempalace/config.ts +10 -4
  55. package/src/mempalace/format.ts +122 -6
  56. package/src/mempalace/hooks.ts +204 -56
  57. package/src/mempalace/installer-helper.ts +18 -4
  58. package/src/mempalace/python/mempalace_bridge.py +128 -3
  59. package/src/mempalace/runtime.ts +53 -16
  60. package/src/mempalace/schema.ts +151 -30
  61. package/src/mempalace/session-summary.ts +5 -0
  62. package/src/mempalace/tool.ts +17 -4
  63. package/src/mempalace/upstream-limits.ts +69 -0
  64. package/src/planning/approval-flow.ts +25 -2
  65. package/src/planning/planning-ask-tool.ts +34 -4
  66. package/src/planning/system-prompt.ts +1 -1
  67. package/src/tool-catalog/active-tool-controller.ts +0 -22
  68. package/src/tool-catalog/active-tool-planner.ts +0 -26
  69. package/src/tool-catalog/tool-groups.ts +1 -9
  70. package/src/types.ts +127 -8
  71. package/src/ui-design/session.ts +114 -8
  72. package/src/utils/executable.ts +10 -1
  73. package/src/workspace/state-paths.ts +1 -1
  74. package/src/commands/mcp.ts +0 -814
  75. package/src/mcp/activation.ts +0 -77
  76. package/src/mcp/config.ts +0 -223
  77. package/src/mcp/docs.ts +0 -154
  78. package/src/mcp/gateway.ts +0 -103
  79. package/src/mcp/lifecycle.ts +0 -79
  80. package/src/mcp/manager-tool.ts +0 -104
  81. package/src/mcp/mcpc.ts +0 -113
  82. package/src/mcp/registry.ts +0 -98
  83. package/src/mcp/triggers.ts +0 -62
  84. package/src/mcp/types.ts +0 -95
@@ -19,6 +19,15 @@ export type BridgePathResult =
19
19
 
20
20
  export interface BridgePathOptions {
21
21
  moduleUrl?: string;
22
+ /**
23
+ * Installed extension package roots. Used when OMP executes temp-copied TS
24
+ * modules that do not preserve adjacent non-TypeScript assets.
25
+ */
26
+ extensionRoots?: string[];
27
+ }
28
+
29
+ export interface BridgePathPlatformPaths {
30
+ agent(...segments: string[]): string;
22
31
  }
23
32
 
24
33
  export interface ProcessRunResult {
@@ -74,6 +83,7 @@ export type RunBridgeRequestResult =
74
83
  stdoutPreview: string;
75
84
  stderrTail: string;
76
85
  durationMs: number;
86
+ completion?: Promise<void>;
77
87
  };
78
88
 
79
89
  export interface RunBridgeRequestOptions {
@@ -132,21 +142,39 @@ export interface SetupMempalaceRuntimeOptions {
132
142
  export function resolveBridgeScriptPath(options: BridgePathOptions = {}): BridgePathResult {
133
143
  const moduleUrl = options.moduleUrl ?? import.meta.url;
134
144
  const runtimePath = fileURLToPath(moduleUrl);
135
- const bridgePath = path.join(path.dirname(runtimePath), "python", "mempalace_bridge.py");
145
+ const moduleBridgePath = path.join(path.dirname(runtimePath), "python", "mempalace_bridge.py");
146
+ if (fs.existsSync(moduleBridgePath)) {
147
+ return { ok: true, path: moduleBridgePath };
148
+ }
136
149
 
137
- if (!fs.existsSync(bridgePath)) {
138
- return {
139
- ok: false,
140
- path: bridgePath,
141
- error: {
142
- code: "bridge_not_found",
143
- message: `Bundled MemPalace bridge not found at ${bridgePath}`,
144
- remediation: "Reinstall supipowers or verify the package includes src/mempalace/python/mempalace_bridge.py.",
145
- },
146
- };
150
+ let missingBridgePath = moduleBridgePath;
151
+ for (const extensionRoot of options.extensionRoots ?? []) {
152
+ const extensionBridgePath = path.join(extensionRoot, "src", "mempalace", "python", "mempalace_bridge.py");
153
+ missingBridgePath = extensionBridgePath;
154
+ if (fs.existsSync(extensionBridgePath)) {
155
+ return { ok: true, path: extensionBridgePath };
156
+ }
147
157
  }
148
158
 
149
- return { ok: true, path: bridgePath };
159
+ return {
160
+ ok: false,
161
+ path: missingBridgePath,
162
+ error: {
163
+ code: "bridge_not_found",
164
+ message: `Bundled MemPalace bridge not found at ${missingBridgePath}`,
165
+ remediation: "Reinstall supipowers or verify the package includes src/mempalace/python/mempalace_bridge.py.",
166
+ },
167
+ };
168
+ }
169
+
170
+ export function resolveInstalledBridgeScriptPath(
171
+ paths: BridgePathPlatformPaths,
172
+ options: BridgePathOptions = {},
173
+ ): BridgePathResult {
174
+ return resolveBridgeScriptPath({
175
+ ...options,
176
+ extensionRoots: [paths.agent("extensions", "supipowers"), ...(options.extensionRoots ?? [])],
177
+ });
150
178
  }
151
179
 
152
180
  export function resolveManagedVenvPaths(
@@ -296,8 +324,14 @@ export async function runBridgeRequest(options: RunBridgeRequestOptions): Promis
296
324
  const input = JSON.stringify(options.request);
297
325
  const runner = options.runner ?? defaultProcessRunner;
298
326
 
327
+ const runnerPromise = Promise.resolve().then(() =>
328
+ runner(options.pythonPath, [options.bridgeScriptPath], { input, timeoutMs: options.timeoutMs }),
329
+ );
330
+ const completion = runnerPromise.then(() => {}, () => {});
331
+
332
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
299
333
  const timeout = new Promise<ProcessRunResult>((resolve) => {
300
- setTimeout(() => resolve({
334
+ timeoutHandle = setTimeout(() => resolve({
301
335
  code: -1,
302
336
  stdout: "",
303
337
  stderr: "",
@@ -307,9 +341,9 @@ export async function runBridgeRequest(options: RunBridgeRequestOptions): Promis
307
341
  let runResult: ProcessRunResult;
308
342
  try {
309
343
  runResult = await Promise.race([
310
- runner(options.pythonPath, [options.bridgeScriptPath], { input, timeoutMs: options.timeoutMs }),
344
+ runnerPromise,
311
345
  timeout,
312
- ]);
346
+ ]).finally(() => clearTimeout(timeoutHandle));
313
347
  } catch (error) {
314
348
  return {
315
349
  ok: false,
@@ -336,6 +370,7 @@ export async function runBridgeRequest(options: RunBridgeRequestOptions): Promis
336
370
  stdoutPreview: "",
337
371
  stderrTail: "",
338
372
  durationMs,
373
+ completion,
339
374
  };
340
375
  }
341
376
 
@@ -550,13 +585,15 @@ export function buildMempalaceCliArgs(
550
585
  const args: string[] = [action];
551
586
  if (action === "split") {
552
587
  if (params.source_file) args.push(params.source_file);
553
- } else if (params.dir) {
588
+ } else if (action !== "repair" && params.dir) {
554
589
  args.push(params.dir);
555
590
  }
556
591
 
557
592
  if (typeof params.limit === "number") args.push("--limit", String(params.limit));
558
593
  if (params.mode) args.push("--mode", params.mode);
594
+ if (action === "repair" && params.source) args.push("--source", params.source);
559
595
  if (params.extract) args.push("--extract");
596
+ if (action === "repair" && params.archive_existing) args.push("--archive-existing");
560
597
  if (params.dry_run) args.push("--dry-run");
561
598
  if (params.include_ignored) args.push("--include-ignored");
562
599
  if (params.no_gitignore) args.push("--no-gitignore");
@@ -1,3 +1,11 @@
1
+ import {
2
+ MEMPALACE_MAX_CONTENT_LENGTH,
3
+ MEMPALACE_MAX_HOPS,
4
+ MEMPALACE_MAX_NAME_LENGTH,
5
+ MEMPALACE_MAX_QUERY_LENGTH,
6
+ MEMPALACE_MAX_RESULTS,
7
+ } from "./upstream-limits.js";
8
+
1
9
  export const MEMPALACE_ACTIONS = [
2
10
  "status",
3
11
  "list_wings",
@@ -35,6 +43,7 @@ export const MEMPALACE_ACTIONS = [
35
43
  "split",
36
44
  "repair",
37
45
  "wake_up",
46
+ "wake_up_and_search",
38
47
  ] as const;
39
48
 
40
49
  export type MempalaceAction = typeof MEMPALACE_ACTIONS[number];
@@ -50,13 +59,14 @@ export interface MempalaceParams {
50
59
  drawer_id?: string;
51
60
  content?: string;
52
61
  source_file?: string;
62
+ source_drawer_id?: string;
53
63
  added_by?: string;
54
64
  subject?: string;
55
65
  predicate?: string;
56
66
  object?: string;
57
67
  valid_from?: string;
58
68
  valid_to?: string;
59
- ended?: boolean;
69
+ ended?: string;
60
70
  as_of?: string;
61
71
  direction?: string;
62
72
  start_room?: string;
@@ -72,8 +82,10 @@ export interface MempalaceParams {
72
82
  topic?: string;
73
83
  dir?: string;
74
84
  mode?: string;
85
+ source?: string;
75
86
  extract?: boolean;
76
87
  dry_run?: boolean;
88
+ archive_existing?: boolean;
77
89
  include_ignored?: boolean;
78
90
  no_gitignore?: boolean;
79
91
  yes?: boolean;
@@ -94,12 +106,14 @@ const STRING_FIELDS = [
94
106
  "drawer_id",
95
107
  "content",
96
108
  "source_file",
109
+ "source_drawer_id",
97
110
  "added_by",
98
111
  "subject",
99
112
  "predicate",
100
113
  "object",
101
114
  "valid_from",
102
115
  "valid_to",
116
+ "ended",
103
117
  "as_of",
104
118
  "direction",
105
119
  "start_room",
@@ -114,34 +128,65 @@ const STRING_FIELDS = [
114
128
  "topic",
115
129
  "dir",
116
130
  "mode",
131
+ "source",
117
132
  ] as const;
118
133
 
119
134
  const POSITIVE_INTEGER_FIELDS = ["limit", "max_hops", "timeout"] as const;
135
+ const MAX_INTEGER_FIELDS: ReadonlyArray<readonly [keyof MempalaceParams, number]> = [
136
+ ["limit", MEMPALACE_MAX_RESULTS],
137
+ ["max_hops", MEMPALACE_MAX_HOPS],
138
+ ];
120
139
  const NON_NEGATIVE_INTEGER_FIELDS = ["offset"] as const;
121
- const BOOLEAN_FIELDS = ["ended", "extract", "dry_run", "include_ignored", "no_gitignore", "yes"] as const;
140
+ const BOOLEAN_FIELDS = ["extract", "dry_run", "archive_existing", "include_ignored", "no_gitignore", "yes"] as const;
141
+
142
+ // Per-field maximum string lengths. Each entry mirrors the JSON schema
143
+ // `maxLength` and corresponds to an upstream MemPalace sanitizer bound:
144
+ // - sanitize_query (tool_search) silently truncates anything beyond
145
+ // MAX_QUERY_LENGTH, so callers can't tell their long query was clipped.
146
+ // - sanitize_name / sanitize_kg_value / sanitize_content all raise
147
+ // ValueError beyond their limits, which surfaces as a domain error
148
+ // from the python bridge instead of at the TS boundary.
149
+ // Enforcing here gives every overlong field the same `field exceeds <N>
150
+ // characters` error path before the python child is spawned.
151
+ const MAX_LENGTH_FIELDS: ReadonlyArray<readonly [keyof MempalaceParams, number]> = [
152
+ ["query", MEMPALACE_MAX_QUERY_LENGTH],
153
+ ["content", MEMPALACE_MAX_CONTENT_LENGTH],
154
+ ["entry", MEMPALACE_MAX_CONTENT_LENGTH],
155
+ ["wing", MEMPALACE_MAX_NAME_LENGTH],
156
+ ["room", MEMPALACE_MAX_NAME_LENGTH],
157
+ ["subject", MEMPALACE_MAX_NAME_LENGTH],
158
+ ["predicate", MEMPALACE_MAX_NAME_LENGTH],
159
+ ["object", MEMPALACE_MAX_NAME_LENGTH],
160
+ ["start_room", MEMPALACE_MAX_NAME_LENGTH],
161
+ ["source_wing", MEMPALACE_MAX_NAME_LENGTH],
162
+ ["source_room", MEMPALACE_MAX_NAME_LENGTH],
163
+ ["target_wing", MEMPALACE_MAX_NAME_LENGTH],
164
+ ["target_room", MEMPALACE_MAX_NAME_LENGTH],
165
+ ["agent_name", MEMPALACE_MAX_NAME_LENGTH],
166
+ ["topic", MEMPALACE_MAX_NAME_LENGTH],
167
+ ] as const;
122
168
 
123
- const REQUIRED_FIELDS: Partial<Record<MempalaceAction, readonly (keyof MempalaceParams)[]>> = {
169
+ export const REQUIRED_FIELDS: Partial<Record<MempalaceAction, readonly (keyof MempalaceParams)[]>> = {
124
170
  search: ["query"],
125
171
  check_duplicate: ["content"],
126
172
  get_drawer: ["drawer_id"],
127
173
  list_drawers: ["wing"],
128
- add_drawer: ["content"],
174
+ add_drawer: ["wing", "room", "content"],
129
175
  update_drawer: ["drawer_id", "content"],
130
176
  delete_drawer: ["drawer_id"],
131
177
  kg_query: ["subject"],
132
178
  kg_add: ["subject", "predicate", "object"],
133
- kg_invalidate: ["subject", "predicate"],
179
+ kg_invalidate: ["subject", "predicate", "object"],
134
180
  kg_timeline: ["subject"],
135
181
  traverse: ["start_room"],
136
- find_tunnels: ["source_wing", "source_room"],
137
182
  create_tunnel: ["source_wing", "source_room", "target_wing", "target_room", "label"],
138
183
  delete_tunnel: ["tunnel_id"],
139
184
  follow_tunnels: ["source_wing", "source_room"],
140
- diary_write: ["entry"],
185
+ diary_write: ["entry", "agent_name"],
186
+ diary_read: ["agent_name"],
141
187
  init: ["dir"],
142
188
  mine: ["dir"],
143
189
  split: ["source_file"],
144
- repair: ["dir"],
145
190
  };
146
191
 
147
192
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -156,6 +201,44 @@ function isNonEmptyString(value: unknown): value is string {
156
201
  return typeof value === "string" && value.trim().length > 0;
157
202
  }
158
203
 
204
+ const ISO_DATE_PART_PATTERN = "\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])";
205
+ const ISO_UTC_DATETIME_PATTERN = `${ISO_DATE_PART_PATTERN}T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|\\+00:00)`;
206
+ const ISO_TEMPORAL_PATTERN = `^(?:${ISO_DATE_PART_PATTERN}|${ISO_UTC_DATETIME_PATTERN})$`;
207
+ const ISO_TEMPORAL_DESCRIPTION =
208
+ "ISO temporal string accepted by MemPalace: YYYY-MM-DD, YYYY-MM-DDTHH:MM:SSZ, or YYYY-MM-DDTHH:MM:SS+00:00.";
209
+ const ISO_TEMPORAL_SCHEMA = {
210
+ type: "string",
211
+ pattern: ISO_TEMPORAL_PATTERN,
212
+ description: ISO_TEMPORAL_DESCRIPTION,
213
+ } as const;
214
+ const ISO_TEMPORAL_REGEX = new RegExp(ISO_TEMPORAL_PATTERN);
215
+ const ISO_TEMPORAL_FIELDS = ["as_of", "valid_from", "valid_to", "ended"] as const;
216
+ const DAYS_IN_MONTH = [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as const;
217
+
218
+ function fixedInt(value: string, start: number, end: number): number {
219
+ let out = 0;
220
+ for (let i = start; i < end; i += 1) {
221
+ out = out * 10 + value.charCodeAt(i) - 48;
222
+ }
223
+ return out;
224
+ }
225
+
226
+ function isLeapYear(year: number): boolean {
227
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
228
+ }
229
+
230
+ function isValidIsoTemporal(value: string): boolean {
231
+ if (!ISO_TEMPORAL_REGEX.test(value)) return false;
232
+
233
+ const year = fixedInt(value, 0, 4);
234
+ const month = fixedInt(value, 5, 7);
235
+ const day = fixedInt(value, 8, 10);
236
+ if (year < 1) return false;
237
+
238
+ const daysInMonth = month === 2 ? (isLeapYear(year) ? 29 : 28) : DAYS_IN_MONTH[month - 1];
239
+ return day <= daysInMonth;
240
+ }
241
+
159
242
  export const mempalaceToolParameters = {
160
243
  type: "object",
161
244
  additionalProperties: false,
@@ -166,38 +249,45 @@ export const mempalaceToolParameters = {
166
249
  description: "Action to dispatch.",
167
250
  },
168
251
  palace: { type: "string" },
169
- wing: { type: "string" },
170
- room: { type: "string" },
171
- query: { type: "string" },
172
- limit: { type: "integer", minimum: 1 },
173
- offset: { type: "integer", minimum: 0 },
252
+ wing: {
253
+ type: "string",
254
+ maxLength: MEMPALACE_MAX_NAME_LENGTH,
255
+ description: "Wing name. Required for list_drawers to scope the query and avoid an accidental full-palace dump.",
256
+ },
257
+ room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
258
+ query: { type: "string", maxLength: MEMPALACE_MAX_QUERY_LENGTH },
259
+ limit: { type: "integer", minimum: 1, maximum: MEMPALACE_MAX_RESULTS },
260
+ offset: { type: "integer", minimum: 0, description: "Zero-based pagination offset (use with limit for list_drawers and similar listing actions)." },
174
261
  drawer_id: { type: "string" },
175
- content: { type: "string" },
262
+ content: { type: "string", maxLength: MEMPALACE_MAX_CONTENT_LENGTH },
176
263
  source_file: { type: "string" },
264
+ source_drawer_id: { type: "string" },
177
265
  added_by: { type: "string" },
178
- subject: { type: "string" },
179
- predicate: { type: "string" },
180
- object: { type: "string" },
181
- valid_from: { type: "string" },
182
- valid_to: { type: "string" },
183
- ended: { type: "boolean" },
184
- as_of: { type: "string" },
266
+ subject: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
267
+ predicate: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
268
+ object: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
269
+ valid_from: ISO_TEMPORAL_SCHEMA,
270
+ valid_to: ISO_TEMPORAL_SCHEMA,
271
+ ended: ISO_TEMPORAL_SCHEMA,
272
+ as_of: ISO_TEMPORAL_SCHEMA,
185
273
  direction: { type: "string" },
186
- start_room: { type: "string" },
187
- max_hops: { type: "integer", minimum: 1 },
188
- source_wing: { type: "string" },
189
- source_room: { type: "string" },
190
- target_wing: { type: "string" },
191
- target_room: { type: "string" },
274
+ start_room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
275
+ max_hops: { type: "integer", minimum: 1, maximum: MEMPALACE_MAX_HOPS },
276
+ source_wing: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
277
+ source_room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
278
+ target_wing: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
279
+ target_room: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
192
280
  label: { type: "string" },
193
281
  tunnel_id: { type: "string" },
194
- agent_name: { type: "string" },
195
- entry: { type: "string" },
196
- topic: { type: "string" },
282
+ agent_name: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
283
+ entry: { type: "string", maxLength: MEMPALACE_MAX_CONTENT_LENGTH },
284
+ topic: { type: "string", maxLength: MEMPALACE_MAX_NAME_LENGTH },
197
285
  dir: { type: "string" },
198
286
  mode: { type: "string" },
287
+ source: { type: "string" },
199
288
  extract: { type: "boolean" },
200
289
  dry_run: { type: "boolean" },
290
+ archive_existing: { type: "boolean" },
201
291
  include_ignored: { type: "boolean" },
202
292
  no_gitignore: { type: "boolean" },
203
293
  yes: { type: "boolean" },
@@ -254,6 +344,37 @@ export function validateMempalaceParams(params: unknown): MempalaceValidationRes
254
344
  }
255
345
  }
256
346
 
347
+ for (const [field, max] of MAX_INTEGER_FIELDS) {
348
+ const value = params[field];
349
+ if (typeof value === "number" && Number.isInteger(value) && value > max) {
350
+ errors.push(`${field} exceeds maximum of ${max}`);
351
+ }
352
+ }
353
+
354
+ for (const [field, max] of MAX_LENGTH_FIELDS) {
355
+ const value = params[field];
356
+ if (typeof value === "string" && value.length > max) {
357
+ errors.push(`${field} exceeds maximum length of ${max} characters`);
358
+ }
359
+ }
360
+
361
+ // ISO temporal fields: callers commonly pass `" 2026-05-13 "` from logs or
362
+ // env vars — strip surrounding whitespace before validating so we accept
363
+ // the same shape MemPalace's downstream sqlite comparison would. Empty
364
+ // strings are still rejected (semantically not a date, and would be stored
365
+ // as garbage in the kg.triples valid_from/valid_to columns).
366
+ for (const field of ISO_TEMPORAL_FIELDS) {
367
+ const value = params[field];
368
+ if (typeof value !== "string") continue;
369
+ const trimmed = value.trim();
370
+ if (trimmed !== value) {
371
+ (params as Record<string, unknown>)[field] = trimmed;
372
+ }
373
+ if (!isValidIsoTemporal(trimmed)) {
374
+ errors.push(`${field} must be an ISO temporal string accepted by MemPalace`);
375
+ }
376
+ }
377
+
257
378
  for (const field of REQUIRED_FIELDS[action] ?? []) {
258
379
  if (!isNonEmptyString(params[field])) {
259
380
  errors.push(`${field} is required for action ${action}`);
@@ -185,6 +185,11 @@ export function buildCompactionCheckpoint(options: SessionSummaryOptions): Compa
185
185
 
186
186
  export function buildShutdownDiary(options: SessionSummaryOptions): ShutdownDiary {
187
187
  const { body, now } = buildBody("MemPalace shutdown diary", "shutdown", options);
188
+ // source_file convention: the bridge's Python layer (mempalace_bridge.py) embeds
189
+ // the source_file value as a deterministic first-line prefix in the entry text:
190
+ // "[source: <source_file>]\n<entry>"
191
+ // tool_diary_write does not accept source_file natively; the prefix is how the
192
+ // round-trip proves the linkage (diary_read returns the full document text).
188
193
  return {
189
194
  entry: body,
190
195
  metadata: {
@@ -5,7 +5,7 @@ import { resolveMempalaceConfig, type ResolvedMempalaceConfig } from "./config.j
5
5
  import { formatMempalaceError, formatMempalaceResult } from "./format.js";
6
6
  import { mempalaceToolParameters, validateMempalaceParams, type MempalaceParams } from "./schema.js";
7
7
  import {
8
- resolveBridgeScriptPath,
8
+ resolveInstalledBridgeScriptPath,
9
9
  setupMempalaceRuntime,
10
10
  type BridgePathResult,
11
11
  type SetupMempalaceRuntimeOptions,
@@ -55,10 +55,11 @@ async function executeSetup(
55
55
  resolved: ResolvedMempalaceConfig,
56
56
  cwd: string,
57
57
  managedBinDir: string,
58
+ defaultResolveBridgeScriptPath: () => BridgePathResult,
58
59
  deps: MempalaceToolDeps,
59
60
  onUpdate: unknown,
60
61
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> }> {
61
- const bridgePath = (deps.resolveBridgeScriptPath ?? resolveBridgeScriptPath)();
62
+ const bridgePath = (deps.resolveBridgeScriptPath ?? defaultResolveBridgeScriptPath)();
62
63
  if (!bridgePath.ok) {
63
64
  const formatted = formatMempalaceError(bridgePath.error, {
64
65
  ok: false,
@@ -121,6 +122,10 @@ export function registerMempalaceTool(
121
122
  return;
122
123
  }
123
124
  if (!snapshot.ready) return;
125
+ const bridgeRuntime = {
126
+ resolveBridgeScriptPath: () => resolveInstalledBridgeScriptPath(platform.paths),
127
+ };
128
+
124
129
 
125
130
  platform.registerTool({
126
131
  name: "mempalace",
@@ -143,12 +148,20 @@ export function registerMempalaceTool(
143
148
  const params = validation.params;
144
149
 
145
150
  if (params.action === "setup") {
146
- return await executeSetup(params, resolved, cwd, platform.paths.global("bin"), deps, onUpdate);
151
+ return await executeSetup(
152
+ params,
153
+ resolved,
154
+ cwd,
155
+ platform.paths.global("bin"),
156
+ bridgeRuntime.resolveBridgeScriptPath,
157
+ deps,
158
+ onUpdate,
159
+ );
147
160
  }
148
161
 
149
162
  const bridge = deps.createBridge
150
163
  ? deps.createBridge(resolved, cwd)
151
- : createMempalaceBridge({ cwd, config: resolved });
164
+ : createMempalaceBridge({ cwd, config: resolved, runtime: bridgeRuntime });
152
165
  const result = await bridge.execute(params);
153
166
 
154
167
  if (!result.ok) {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Upstream MemPalace pin.
3
+ *
4
+ * Single source of truth for the `mempalace` PyPI package version and
5
+ * the parameter limits the upstream package enforces. Keeping these in
6
+ * one place — instead of sprinkling `"3.3.4"` / `500` / `100` / `128`
7
+ * magic literals across the config defaults, schema, hooks, and tests —
8
+ * makes a version bump a one-line edit and guarantees our tool surface
9
+ * advertises the same bounds the upstream MCP server enforces.
10
+ *
11
+ * # Bump procedure
12
+ * 1. Update `MEMPALACE_PACKAGE_VERSION` below.
13
+ * 2. Re-verify each `MEMPALACE_MAX_*` constant against the cited
14
+ * upstream source path. Update any that drifted.
15
+ * 3. If the upstream MCP API surface (function names, parameter names)
16
+ * changed, update the dispatch table in
17
+ * `src/mempalace/python/mempalace_bridge.py` and its header comment.
18
+ * 4. Run `bun ci`. All consumers — including tests — read from these
19
+ * constants, so a mismatch surfaces as a test failure rather than
20
+ * silent runtime drift.
21
+ */
22
+
23
+ /**
24
+ * Exact PyPI version installed by the managed setup pipeline. Flows into
25
+ * `DEFAULT_CONFIG.mempalace.packageVersion` and, from there, into the
26
+ * `mempalace==<version>` argument handed to `uv pip install`.
27
+ */
28
+ export const MEMPALACE_PACKAGE_VERSION = "3.3.5";
29
+
30
+ /**
31
+ * Upper bound applied internally by `tool_search` and `tool_list_drawers`
32
+ * to the `limit` argument. Any value above this is silently clamped.
33
+ *
34
+ * Source: `mempalace/mcp_server.py` `_MAX_RESULTS = 100`.
35
+ */
36
+ export const MEMPALACE_MAX_RESULTS = 100;
37
+
38
+ /**
39
+ * Maximum search-query length. `tool_search` runs `sanitize_query`, which
40
+ * truncates anything over this threshold (worst case: keeps only the
41
+ * trailing N characters). Above this, prompt-contamination patterns
42
+ * start dominating the embedding signal — see upstream Issue #333.
43
+ *
44
+ * Source: `mempalace/query_sanitizer.py` `MAX_QUERY_LENGTH = 250`.
45
+ */
46
+ export const MEMPALACE_MAX_QUERY_LENGTH = 250;
47
+
48
+ /**
49
+ * Maximum length for wing / room / predicate / entity-style identifiers.
50
+ * `sanitize_name` and `sanitize_kg_value` raise `ValueError` above this.
51
+ *
52
+ * Source: `mempalace/config.py` `MAX_NAME_LENGTH = 128`.
53
+ */
54
+ export const MEMPALACE_MAX_NAME_LENGTH = 128;
55
+
56
+ /**
57
+ * Maximum drawer / diary content length. `sanitize_content` defaults
58
+ * to this when no explicit override is passed.
59
+ *
60
+ * Source: `mempalace/config.py` `sanitize_content(..., max_length: int = 100_000)`.
61
+ */
62
+ export const MEMPALACE_MAX_CONTENT_LENGTH = 100_000;
63
+
64
+ /**
65
+ * Upper bound applied internally by `tool_traverse_graph` to `max_hops`.
66
+ *
67
+ * Source: `mempalace/mcp_server.py` `tool_traverse_graph` — `max(1, min(max_hops, 10))`.
68
+ */
69
+ export const MEMPALACE_MAX_HOPS = 10;
@@ -186,7 +186,7 @@ export function buildTodoWriteOpsForPlan(plan: Plan): { ops: TodoWriteOp[] } {
186
186
  * When `plan` is provided and has tasks, the prompt also embeds the
187
187
  * exact `todo_write` payload the agent must call before doing any work.
188
188
  */
189
- function buildExecutionPrompt(
189
+ export function buildExecutionPrompt(
190
190
  planContent: string,
191
191
  planPath: string,
192
192
  plan?: Plan,
@@ -313,7 +313,7 @@ async function executeApproveFlow(
313
313
  */
314
314
  export function registerPlanApprovalHook(platform: Platform): void {
315
315
  platform.on("agent_end", async (_event: any, ctx: any) => {
316
- if (!planningActive || !ctx?.hasUI || approvalPending) return;
316
+ if (!planningActive || approvalPending) return;
317
317
 
318
318
  // Detect newly written plan files
319
319
  const plansNow = listPlans(platform.paths, planCwd);
@@ -407,6 +407,29 @@ export function registerPlanApprovalHook(platform: Platform): void {
407
407
  cwd: planCwd,
408
408
  });
409
409
  } catch {}
410
+ if (!ctx?.hasUI) {
411
+ const message = [
412
+ `Plan saved to \`${planPath}\`.`,
413
+ "Interactive approval is unavailable in this runtime, so no execution was started.",
414
+ `To continue manually, explicitly send: \`Execute the saved plan at ${planPath} step by step; verify each step before proceeding.\``,
415
+ ].join("\n");
416
+ debugLogger?.log("approval_flow_no_ui", {
417
+ planName,
418
+ planPath,
419
+ });
420
+ ctx?.ui?.notify?.("Plan saved; interactive approval is required before execution.", "warning");
421
+ platform.sendMessage(
422
+ {
423
+ customType: "supi-plan-awaiting-interactive-approval",
424
+ content: [{ type: "text", text: message }],
425
+ display: true,
426
+ },
427
+ { deliverAs: "steer", triggerTurn: false },
428
+ );
429
+ cancelPlanTracking();
430
+ return;
431
+ }
432
+
410
433
  const approvalOptions = [
411
434
  "Approve and execute",
412
435
  "Refine plan",
@@ -4,10 +4,16 @@ import { isUiDesignActive, recordUiDesignReviewApproval } from "../ui-design/ses
4
4
 
5
5
  /**
6
6
  * Register a `planning_ask` tool — identical to the built-in `ask` tool
7
- * but with **no timeout**. OMP's built-in ask tool applies the user's
8
- * `ask.timeout` setting (default 30s) and only disables it for OMP's
9
- * native plan mode. Since `/supi:plan` is not native plan mode, planning
10
- * questions would auto-dismiss. This tool bypasses that limitation.
7
+ * but with **no timeout**, regardless of the user's `ask.timeout` setting.
8
+ * OMP 14.9.5 changed the `ask.timeout` default from 30s to 0 (wait
9
+ * indefinitely), but a user-configured non-zero value still applies to the
10
+ * generic `ask` tool; this wrapper keeps planning-mode questions blocking
11
+ * for any such configuration.
12
+ *
13
+ * Also records the chosen option into the ui-design session ledger via
14
+ * `recordUiDesignReviewApproval` and pairs with
15
+ * `registerPlanningAskToolGuard`, which redirects generic `ask` calls back
16
+ * to this tool during planning / ui-design sessions.
11
17
  *
12
18
  * The tool is always registered (lightweight) but the planning system
13
19
  * prompt directs the model to use it only during planning sessions.
@@ -62,6 +68,22 @@ export function registerPlanningAskTool(platform: Platform): void {
62
68
  };
63
69
  }
64
70
 
71
+ if (ctx?.hasUI === false || typeof ctx?.ui?.select !== "function") {
72
+ const result = {
73
+ error: "interactive_planning_question_unavailable",
74
+ message: "Interactive planning questions cannot be answered in this runtime. Present this question and its options to the user instead of choosing a default.",
75
+ question: params.question,
76
+ options: labels,
77
+ recommended: params.recommended ?? null,
78
+ };
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify(result) }],
81
+ details: result,
82
+ error: true,
83
+ };
84
+ }
85
+
86
+
65
87
  const choice = await ctx.ui.select(params.question, labels, {
66
88
  initialIndex: params.recommended,
67
89
  // No timeout — planning decisions need unlimited time
@@ -107,6 +129,14 @@ function getAskRedirectReason(): string | null {
107
129
  */
108
130
  export function registerPlanningAskToolGuard(platform: Platform): void {
109
131
  platform.on("tool_call", (event) => {
132
+ if (event.toolName === "exit_plan_mode" && isPlanningActive()) {
133
+ return {
134
+ block: true,
135
+ reason:
136
+ "Planning mode: /supi:plan uses a file-based approval hook. Do not call exit_plan_mode because it is OMP's native approval path and bypasses supipowers plan tracking.",
137
+ };
138
+ }
139
+
110
140
  if (event.toolName !== "ask") return;
111
141
 
112
142
  const reason = getAskRedirectReason();
@@ -233,7 +233,7 @@ function buildPlanningCriticalBlock(options: PlanningSystemPromptOptions): strin
233
233
  "## Plan submission",
234
234
  "",
235
235
  "This is NOT native OMP plan mode.",
236
- "You **MUST NOT** call `exit_plan_mode` or `ExitPlanMode` — it will fail.",
236
+ "You **MUST NOT** call `exit_plan_mode` or `ExitPlanMode` — that is OMP's native approval path and bypasses supipowers' file-based approval hook.",
237
237
  `You **MUST NOT** write plans to \`local://PLAN.md\` — that is OMP's native plan location and will not trigger the approval flow.`,
238
238
  `You **MUST** save the plan to \`${options.plansDir}/YYYY-MM-DD-<feature-name>.md\` using the Write tool.`,
239
239
  "After saving, tell the user the plan path, then **stop and yield your turn**.",