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.
- package/README.md +5 -6
- package/package.json +4 -2
- package/skills/harness/SKILL.md +1 -0
- package/src/bootstrap.ts +8 -133
- package/src/commands/optimize-context.ts +153 -16
- package/src/commands/runbook.ts +511 -0
- package/src/config/defaults.ts +5 -5
- package/src/config/loader.ts +1 -0
- package/src/config/schema.ts +2 -6
- package/src/context/rule-renderer.ts +274 -2
- package/src/context/runbook-extension-template.ts +193 -0
- package/src/context/startup-check.ts +197 -2
- package/src/context/startup-optimizer.ts +133 -10
- package/src/context-mode/knowledge/store.ts +381 -43
- package/src/context-mode/tools.ts +41 -3
- package/src/deps/registry.ts +1 -12
- package/src/fix-pr/assessment.ts +1 -0
- package/src/fix-pr/prompt-builder.ts +1 -0
- package/src/git/commit.ts +76 -18
- package/src/harness/command.ts +201 -12
- package/src/harness/default-agents/docs.md +39 -0
- package/src/harness/docs/config.ts +29 -0
- package/src/harness/docs/glob-match.ts +27 -0
- package/src/harness/docs/index-renderer.ts +82 -0
- package/src/harness/docs/provenance.ts +125 -0
- package/src/harness/docs/regen-decision.ts +167 -0
- package/src/harness/docs/representative-files.ts +175 -0
- package/src/harness/docs/source-hash.ts +106 -0
- package/src/harness/docs/validator.ts +233 -0
- package/src/harness/git-verification.ts +515 -0
- package/src/harness/git-verify-qa.ts +406 -0
- package/src/harness/hooks/layer-context-inject.ts +35 -1
- package/src/harness/hooks/register.ts +24 -3
- package/src/harness/pipeline.ts +37 -13
- package/src/harness/pr-comment/baseline.ts +105 -0
- package/src/harness/pr-comment/ci-env.ts +120 -0
- package/src/harness/pr-comment/gh-poster.ts +227 -0
- package/src/harness/pr-comment/handler.ts +198 -0
- package/src/harness/pr-comment/render.ts +297 -0
- package/src/harness/pr-comment/status.ts +95 -0
- package/src/harness/pr-comment/types.ts +73 -0
- package/src/harness/pr-comment/workflow-summary.ts +47 -0
- package/src/harness/project-paths.ts +95 -0
- package/src/harness/stages/design.ts +1 -0
- package/src/harness/stages/discover.ts +1 -13
- package/src/harness/stages/docs.ts +708 -0
- package/src/harness/stages/implement-apply.ts +934 -0
- package/src/harness/stages/implement.ts +64 -51
- package/src/harness/stages/plan.ts +25 -16
- package/src/harness/stages/validate.ts +478 -0
- package/src/harness/storage.ts +142 -0
- package/src/harness/tools.ts +130 -0
- package/src/mempalace/bridge.ts +207 -41
- package/src/mempalace/config.ts +10 -4
- package/src/mempalace/format.ts +122 -6
- package/src/mempalace/hooks.ts +204 -56
- package/src/mempalace/installer-helper.ts +18 -4
- package/src/mempalace/python/mempalace_bridge.py +128 -3
- package/src/mempalace/runtime.ts +53 -16
- package/src/mempalace/schema.ts +151 -30
- package/src/mempalace/session-summary.ts +5 -0
- package/src/mempalace/tool.ts +17 -4
- package/src/mempalace/upstream-limits.ts +69 -0
- package/src/planning/approval-flow.ts +25 -2
- package/src/planning/planning-ask-tool.ts +34 -4
- package/src/planning/system-prompt.ts +1 -1
- package/src/tool-catalog/active-tool-controller.ts +0 -22
- package/src/tool-catalog/active-tool-planner.ts +0 -26
- package/src/tool-catalog/tool-groups.ts +1 -9
- package/src/types.ts +127 -8
- package/src/ui-design/session.ts +114 -8
- package/src/utils/executable.ts +10 -1
- package/src/workspace/state-paths.ts +1 -1
- package/src/commands/mcp.ts +0 -814
- package/src/mcp/activation.ts +0 -77
- package/src/mcp/config.ts +0 -223
- package/src/mcp/docs.ts +0 -154
- package/src/mcp/gateway.ts +0 -103
- package/src/mcp/lifecycle.ts +0 -79
- package/src/mcp/manager-tool.ts +0 -104
- package/src/mcp/mcpc.ts +0 -113
- package/src/mcp/registry.ts +0 -98
- package/src/mcp/triggers.ts +0 -62
- package/src/mcp/types.ts +0 -95
package/src/mempalace/runtime.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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 {
|
|
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
|
-
|
|
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");
|
package/src/mempalace/schema.ts
CHANGED
|
@@ -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?:
|
|
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 = ["
|
|
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: {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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:
|
|
182
|
-
valid_to:
|
|
183
|
-
ended:
|
|
184
|
-
as_of:
|
|
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: {
|
package/src/mempalace/tool.ts
CHANGED
|
@@ -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
|
-
|
|
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 ??
|
|
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(
|
|
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 ||
|
|
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
|
|
8
|
-
* `ask.timeout`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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` —
|
|
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**.",
|