patchrelay 0.36.5 → 0.36.7
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 +3 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +7 -7
- package/dist/cli/commands/setup.js +32 -27
- package/dist/cli/help.js +1 -1
- package/dist/cli/service-commands.js +11 -0
- package/dist/config.js +48 -0
- package/dist/patchrelay-customization.js +68 -0
- package/dist/prompting/patchrelay.js +552 -0
- package/dist/run-orchestrator.js +28 -560
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -161,6 +161,8 @@ PatchRelay uses repo-local workflow files as prompts for Codex runs:
|
|
|
161
161
|
|
|
162
162
|
These files define how the agent should work in that repository. Keep them short, action-oriented, and human-authored.
|
|
163
163
|
|
|
164
|
+
The built-in PatchRelay prompt scaffold lives in `src/prompting/patchrelay.ts`. It is intentionally small: task objective, scope discipline, reactive repair context, workflow guidance, and publication contract. Installation-level and repo-level prompt config can add one extra instructions file or replace a small set of policy sections. See [`docs/prompting.md`](./docs/prompting.md).
|
|
165
|
+
|
|
164
166
|
## Knowledge And Validation Surfaces
|
|
165
167
|
|
|
166
168
|
PatchRelay works best when repository guidance follows progressive disclosure:
|
|
@@ -306,6 +308,7 @@ Useful commands:
|
|
|
306
308
|
- `patchrelay issue path APP-123 --cd`
|
|
307
309
|
- `patchrelay issue open APP-123`
|
|
308
310
|
- `patchrelay issue retry APP-123`
|
|
311
|
+
- `patchrelay service restart`
|
|
309
312
|
- `patchrelay service logs --lines 100`
|
|
310
313
|
|
|
311
314
|
PatchRelay's operator surface is being reduced to its own runtime responsibilities: issue status,
|
package/dist/build-info.json
CHANGED
|
@@ -44,13 +44,13 @@ export async function collectClusterHealth(config, db, runCommand) {
|
|
|
44
44
|
? await probeOptionalService(runCommand, "review-quill", {
|
|
45
45
|
healthy: (payload) => {
|
|
46
46
|
const parsed = payload;
|
|
47
|
-
return parsed.health?.ok === true && parsed.systemd?.ActiveState === "active";
|
|
47
|
+
return parsed.health?.reachable === true && parsed.health?.ok === true && parsed.systemd?.ActiveState === "active";
|
|
48
48
|
},
|
|
49
49
|
summarize: (payload) => {
|
|
50
50
|
const parsed = payload;
|
|
51
|
-
return parsed.health?.ok === true
|
|
52
|
-
?
|
|
53
|
-
: `Unhealthy (${parsed.
|
|
51
|
+
return parsed.health?.reachable === true && parsed.health?.ok === true
|
|
52
|
+
? "Healthy"
|
|
53
|
+
: `Unhealthy (${parsed.health?.reachable === false ? "service not reachable" : "service health unavailable"})`;
|
|
54
54
|
},
|
|
55
55
|
})
|
|
56
56
|
: undefined;
|
|
@@ -68,13 +68,13 @@ export async function collectClusterHealth(config, db, runCommand) {
|
|
|
68
68
|
? await probeOptionalService(runCommand, "merge-steward", {
|
|
69
69
|
healthy: (payload) => {
|
|
70
70
|
const parsed = payload;
|
|
71
|
-
return parsed.systemd?.ActiveState === "active";
|
|
71
|
+
return parsed.health?.reachable === true && parsed.health?.ok === true && parsed.systemd?.ActiveState === "active";
|
|
72
72
|
},
|
|
73
73
|
summarize: (payload) => {
|
|
74
74
|
const parsed = payload;
|
|
75
|
-
return parsed.systemd?.ActiveState === "active"
|
|
75
|
+
return parsed.health?.reachable === true && parsed.health?.ok === true && parsed.systemd?.ActiveState === "active"
|
|
76
76
|
? "Healthy"
|
|
77
|
-
: `Unhealthy (${parsed.systemd?.ActiveState ?? "unknown"})`;
|
|
77
|
+
: `Unhealthy (${parsed.health?.reachable === false ? "service not reachable" : parsed.systemd?.ActiveState ?? "unknown"})`;
|
|
78
78
|
},
|
|
79
79
|
})
|
|
80
80
|
: undefined;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getSystemdUnitPath, } from "../../runtime-paths.js";
|
|
2
2
|
import { initializePatchRelayHome, installServiceUnits } from "../../install.js";
|
|
3
3
|
import { loadConfig } from "../../config.js";
|
|
4
4
|
import { parsePositiveIntegerFlag } from "../args.js";
|
|
5
5
|
import { CliUsageError } from "../errors.js";
|
|
6
6
|
import { formatJson } from "../formatters/json.js";
|
|
7
7
|
import { writeOutput } from "../output.js";
|
|
8
|
-
import { installServiceCommands,
|
|
8
|
+
import { installServiceCommands, runServiceCommands, runSystemctl, tryManageService } from "../service-commands.js";
|
|
9
9
|
export async function handleInitCommand(params) {
|
|
10
10
|
try {
|
|
11
11
|
const requestedPublicBaseUrl = typeof params.parsed.flags.get("public-base-url") === "string"
|
|
@@ -113,24 +113,25 @@ export async function handleInstallServiceCommand(params) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
export async function handleRestartServiceCommand(params) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
116
|
+
const daemonReload = await runSystemctl(params.runCommand, ["daemon-reload"]);
|
|
117
|
+
const restart = await runSystemctl(params.runCommand, ["reload-or-restart", "patchrelay.service"]);
|
|
118
|
+
const ok = daemonReload.ok && restart.ok;
|
|
119
|
+
writeOutput(ok ? params.stdout : params.stderr, params.json
|
|
120
|
+
? formatJson({
|
|
121
|
+
service: "patchrelay",
|
|
122
|
+
unit: "patchrelay.service",
|
|
123
|
+
daemonReloaded: daemonReload.ok,
|
|
124
|
+
restarted: restart.ok,
|
|
125
|
+
errors: [
|
|
126
|
+
...(daemonReload.ok ? [] : [daemonReload.error]),
|
|
127
|
+
...(restart.ok ? [] : [restart.error]),
|
|
128
|
+
],
|
|
129
|
+
})
|
|
130
|
+
: [
|
|
131
|
+
daemonReload.ok ? "systemd daemon-reload completed." : `systemd daemon-reload failed: ${daemonReload.error}`,
|
|
132
|
+
restart.ok ? "Restarted patchrelay.service" : `Restart failed: ${restart.error}`,
|
|
133
|
+
].join("\n") + "\n");
|
|
134
|
+
return ok ? 0 : 1;
|
|
134
135
|
}
|
|
135
136
|
function parseSystemctlShowOutput(raw) {
|
|
136
137
|
const properties = {};
|
|
@@ -148,23 +149,28 @@ function parseSystemctlShowOutput(raw) {
|
|
|
148
149
|
async function readPatchRelayHealth() {
|
|
149
150
|
try {
|
|
150
151
|
const config = loadConfig(undefined, { profile: "doctor" });
|
|
151
|
-
const
|
|
152
|
-
|
|
152
|
+
const host = config.server.bind === "0.0.0.0" ? "127.0.0.1" : config.server.bind;
|
|
153
|
+
const response = await fetch(`http://${host}:${config.server.port}${config.server.healthPath}`, { signal: AbortSignal.timeout(2000) });
|
|
154
|
+
let ok = response.ok;
|
|
153
155
|
try {
|
|
154
|
-
body = await response.json();
|
|
156
|
+
const body = await response.json();
|
|
157
|
+
if (typeof body.ok === "boolean") {
|
|
158
|
+
ok = response.ok && body.ok;
|
|
159
|
+
}
|
|
155
160
|
}
|
|
156
161
|
catch {
|
|
157
|
-
|
|
162
|
+
ok = response.ok;
|
|
158
163
|
}
|
|
159
164
|
return {
|
|
160
165
|
reachable: true,
|
|
166
|
+
ok,
|
|
161
167
|
status: response.status,
|
|
162
|
-
...(body ? { body } : {}),
|
|
163
168
|
};
|
|
164
169
|
}
|
|
165
170
|
catch (error) {
|
|
166
171
|
return {
|
|
167
172
|
reachable: false,
|
|
173
|
+
ok: false,
|
|
168
174
|
error: error instanceof Error ? error.message : String(error),
|
|
169
175
|
};
|
|
170
176
|
}
|
|
@@ -201,7 +207,6 @@ export async function handleServiceCommand(params) {
|
|
|
201
207
|
const payload = {
|
|
202
208
|
service: "patchrelay",
|
|
203
209
|
unit: "patchrelay.service",
|
|
204
|
-
unitPath: getSystemdUnitPath(),
|
|
205
210
|
systemd: properties,
|
|
206
211
|
health,
|
|
207
212
|
};
|
|
@@ -217,7 +222,7 @@ export async function handleServiceCommand(params) {
|
|
|
217
222
|
`Unit path: ${properties.FragmentPath || getSystemdUnitPath()}`,
|
|
218
223
|
properties.ExecMainPID ? `Main PID: ${properties.ExecMainPID}` : undefined,
|
|
219
224
|
health.reachable
|
|
220
|
-
? `Health:
|
|
225
|
+
? `Health: ${health.ok ? "ok" : "unhealthy"} (HTTP ${health.status})`
|
|
221
226
|
: `Health: not reachable (${health.error})`,
|
|
222
227
|
]
|
|
223
228
|
.filter(Boolean)
|
package/dist/cli/help.js
CHANGED
|
@@ -165,7 +165,7 @@ export function serviceHelpText() {
|
|
|
165
165
|
"Commands:",
|
|
166
166
|
" install [--force] [--write-only] [--json] Reinstall the systemd service unit",
|
|
167
167
|
" restart [--json] Reload-or-restart the service",
|
|
168
|
-
" status [--json] Show systemd state and
|
|
168
|
+
" status [--json] Show systemd state and local health",
|
|
169
169
|
" logs [--lines <count>] [--json] Show recent journal logs",
|
|
170
170
|
"",
|
|
171
171
|
"Examples:",
|
|
@@ -23,6 +23,17 @@ export async function tryManageService(runner, commands) {
|
|
|
23
23
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
export async function runSystemctl(runner, args) {
|
|
27
|
+
const result = await runner("sudo", ["systemctl", ...args]);
|
|
28
|
+
if (result.exitCode === 0) {
|
|
29
|
+
return { ok: true, result };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
error: `Command failed with exit code ${result.exitCode}: sudo systemctl ${args.join(" ")}${summarizeCommandOutput(result)}`,
|
|
34
|
+
result,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
26
37
|
export function installServiceCommands() {
|
|
27
38
|
return [
|
|
28
39
|
{ command: "sudo", args: ["systemctl", "daemon-reload"] },
|
package/dist/config.js
CHANGED
|
@@ -59,6 +59,17 @@ const repositorySchema = z.object({
|
|
|
59
59
|
merge_queue_check_name: z.string().min(1).optional(),
|
|
60
60
|
}).optional(),
|
|
61
61
|
});
|
|
62
|
+
const promptLayerSchema = z.object({
|
|
63
|
+
extra_instructions_file: z.string().min(1).optional(),
|
|
64
|
+
replace_sections: z.record(z.string().min(1), z.string().min(1)).default({}),
|
|
65
|
+
});
|
|
66
|
+
const promptByRunTypeSchema = z.object({
|
|
67
|
+
implementation: promptLayerSchema.optional(),
|
|
68
|
+
review_fix: promptLayerSchema.optional(),
|
|
69
|
+
branch_upkeep: promptLayerSchema.optional(),
|
|
70
|
+
ci_repair: promptLayerSchema.optional(),
|
|
71
|
+
queue_repair: promptLayerSchema.optional(),
|
|
72
|
+
});
|
|
62
73
|
const configSchema = z.object({
|
|
63
74
|
server: z.object({
|
|
64
75
|
bind: z.string().default("127.0.0.1"),
|
|
@@ -121,6 +132,17 @@ const configSchema = z.object({
|
|
|
121
132
|
experimental_raw_events: z.boolean().default(true),
|
|
122
133
|
}),
|
|
123
134
|
}),
|
|
135
|
+
prompting: z.object({
|
|
136
|
+
default: promptLayerSchema.default({
|
|
137
|
+
replace_sections: {},
|
|
138
|
+
}),
|
|
139
|
+
by_run_type: promptByRunTypeSchema.default({}),
|
|
140
|
+
}).default({
|
|
141
|
+
default: {
|
|
142
|
+
replace_sections: {},
|
|
143
|
+
},
|
|
144
|
+
by_run_type: {},
|
|
145
|
+
}),
|
|
124
146
|
repos: z.object({
|
|
125
147
|
root: z.string().min(1).default(path.join(homedir(), "projects")),
|
|
126
148
|
}).default(() => ({ root: path.join(homedir(), "projects") })),
|
|
@@ -164,6 +186,7 @@ function withSectionDefaults(input) {
|
|
|
164
186
|
logging: {},
|
|
165
187
|
database: {},
|
|
166
188
|
operator_api: {},
|
|
189
|
+
prompting: {},
|
|
167
190
|
repos: {},
|
|
168
191
|
projects: [],
|
|
169
192
|
repositories: [],
|
|
@@ -181,6 +204,24 @@ function withSectionDefaults(input) {
|
|
|
181
204
|
function resolveRepoSettingsPath(repoPath) {
|
|
182
205
|
return path.join(repoPath, REPO_SETTINGS_DIRNAME, REPO_SETTINGS_FILENAME);
|
|
183
206
|
}
|
|
207
|
+
function readPromptFile(configDir, filePath) {
|
|
208
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath);
|
|
209
|
+
if (!existsSync(resolvedPath)) {
|
|
210
|
+
throw new Error(`Prompt file not found: ${resolvedPath}`);
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
sourcePath: resolvedPath,
|
|
214
|
+
content: readFileSync(resolvedPath, "utf8").trim(),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function loadPromptLayer(configDir, layer) {
|
|
218
|
+
return {
|
|
219
|
+
...(layer.extra_instructions_file
|
|
220
|
+
? { extraInstructions: readPromptFile(configDir, layer.extra_instructions_file) }
|
|
221
|
+
: {}),
|
|
222
|
+
replaceSections: Object.fromEntries(Object.entries(layer.replace_sections).map(([sectionId, fragmentPath]) => [sectionId, readPromptFile(configDir, fragmentPath)])),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
184
225
|
function expandEnv(value, env) {
|
|
185
226
|
if (typeof value === "string") {
|
|
186
227
|
return value.replace(/\$\{([A-Z0-9_]+)(?::-(.*?))?\}/g, (_match, name, fallback) => {
|
|
@@ -322,6 +363,7 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
322
363
|
};
|
|
323
364
|
const parsedFile = parseJsonFile(requestedPath, "config file");
|
|
324
365
|
const parsed = configSchema.parse(withSectionDefaults(expandEnv(parsedFile, env)));
|
|
366
|
+
const configDir = path.dirname(requestedPath);
|
|
325
367
|
const requirements = getLoadProfileRequirements(profile);
|
|
326
368
|
const rWebhookSecret = resolveSecretWithSource("linear-webhook-secret", parsed.linear.webhook_secret_env, env);
|
|
327
369
|
const rTokenEncryptionKey = resolveSecretWithSource("token-encryption-key", parsed.linear.token_encryption_key_env, env);
|
|
@@ -487,6 +529,12 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
|
|
|
487
529
|
experimentalRawEvents: parsed.runner.codex.experimental_raw_events,
|
|
488
530
|
},
|
|
489
531
|
},
|
|
532
|
+
prompting: {
|
|
533
|
+
default: loadPromptLayer(configDir, parsed.prompting.default),
|
|
534
|
+
byRunType: Object.fromEntries(Object.entries(parsed.prompting.by_run_type)
|
|
535
|
+
.filter(([, layer]) => Boolean(layer))
|
|
536
|
+
.map(([runType, layer]) => [runType, loadPromptLayer(configDir, layer)])),
|
|
537
|
+
},
|
|
490
538
|
repos: {
|
|
491
539
|
root: reposRoot,
|
|
492
540
|
},
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const promptLayerSchema = z.object({
|
|
5
|
+
extraInstructionsFile: z.string().min(1).optional(),
|
|
6
|
+
replaceSections: z.record(z.string().min(1), z.string().min(1)).default({}),
|
|
7
|
+
});
|
|
8
|
+
const promptByRunTypeSchema = z.object({
|
|
9
|
+
implementation: promptLayerSchema.optional(),
|
|
10
|
+
review_fix: promptLayerSchema.optional(),
|
|
11
|
+
branch_upkeep: promptLayerSchema.optional(),
|
|
12
|
+
ci_repair: promptLayerSchema.optional(),
|
|
13
|
+
queue_repair: promptLayerSchema.optional(),
|
|
14
|
+
});
|
|
15
|
+
const patchRelayCustomizationSchema = z.object({
|
|
16
|
+
version: z.literal(1),
|
|
17
|
+
prompt: z.object({
|
|
18
|
+
default: promptLayerSchema.default({
|
|
19
|
+
replaceSections: {},
|
|
20
|
+
}),
|
|
21
|
+
byRunType: promptByRunTypeSchema.default({}),
|
|
22
|
+
}).default({
|
|
23
|
+
default: {
|
|
24
|
+
replaceSections: {},
|
|
25
|
+
},
|
|
26
|
+
byRunType: {},
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
function configPath(repoRoot) {
|
|
30
|
+
return path.join(repoRoot, ".patchrelay", "patchrelay.json");
|
|
31
|
+
}
|
|
32
|
+
function readPromptFile(repoRoot, filePath) {
|
|
33
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(repoRoot, filePath);
|
|
34
|
+
if (!existsSync(resolvedPath)) {
|
|
35
|
+
throw new Error(`Prompt file not found: ${resolvedPath}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
sourcePath: resolvedPath,
|
|
39
|
+
content: readFileSync(resolvedPath, "utf8").trim(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function loadPromptLayer(repoRoot, layer) {
|
|
43
|
+
return {
|
|
44
|
+
...(layer?.extraInstructionsFile
|
|
45
|
+
? { extraInstructions: readPromptFile(repoRoot, layer.extraInstructionsFile) }
|
|
46
|
+
: {}),
|
|
47
|
+
replaceSections: Object.fromEntries(Object.entries(layer?.replaceSections ?? {})
|
|
48
|
+
.map(([sectionId, fragmentPath]) => [sectionId, readPromptFile(repoRoot, fragmentPath)])),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function loadPatchRelayRepoPrompting(params) {
|
|
52
|
+
try {
|
|
53
|
+
const filePath = configPath(params.repoRoot);
|
|
54
|
+
if (!existsSync(filePath)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const raw = readFileSync(filePath, "utf8");
|
|
58
|
+
const parsed = patchRelayCustomizationSchema.parse(JSON.parse(raw));
|
|
59
|
+
return {
|
|
60
|
+
default: loadPromptLayer(params.repoRoot, parsed.prompt.default),
|
|
61
|
+
byRunType: Object.fromEntries(Object.entries(parsed.prompt.byRunType).map(([runType, layer]) => [runType, loadPromptLayer(params.repoRoot, layer)])),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
params.logger.warn({ error: error instanceof Error ? error.message : String(error), repoRoot: params.repoRoot }, "PatchRelay repo prompt customization could not be loaded");
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|