@jaybeeuu/agent-cortex 2.7.0 → 2.9.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/CHANGELOG.md +412 -0
- package/README.md +87 -8
- package/agents/README.md +4 -4
- package/agents/guppi/agent.md +162 -0
- package/agents/guppi/claude/frontmatter.json +6 -0
- package/agents/guppi/pi/frontmatter.json +6 -0
- package/agents/plan/agent.md +6 -0
- package/agents/ralph/agent.md +17 -16
- package/agents/ralph/claude/wake.md +4 -4
- package/agents/ralph/pi/wake.md +15 -20
- package/agents/ralph-plan/agent.md +6 -0
- package/bin/agent-cortex.mjs +20 -1
- package/bin/installers/claude.mjs +1 -20
- package/bin/installers/pi.mjs +8 -2
- package/extensions/interrupt-bookmark/README.md +129 -0
- package/extensions/interrupt-bookmark/index.test.ts +717 -0
- package/extensions/interrupt-bookmark/index.ts +354 -0
- package/extensions/interrupt-bookmark/package.json +20 -0
- package/extensions/post-merge-watch/README.md +133 -0
- package/extensions/post-merge-watch/index.test.ts +247 -0
- package/extensions/post-merge-watch/index.ts +123 -0
- package/extensions/post-merge-watch/package.json +16 -0
- package/extensions/post-merge-watch/watch.test.ts +469 -0
- package/extensions/post-merge-watch/watch.ts +431 -0
- package/extensions/session-clear/README.md +74 -0
- package/extensions/session-clear/index.test.ts +524 -0
- package/extensions/session-clear/index.ts +159 -0
- package/extensions/session-clear/package.json +16 -0
- package/extensions/skill-stats/README.md +84 -3
- package/extensions/skill-stats/index.ts +62 -3
- package/extensions/skill-stats/package.json +1 -1
- package/extensions/skill-stats/rollup.test.ts +643 -0
- package/extensions/skill-stats/rollup.ts +415 -0
- package/extensions/skill-stats/timeline.ts +101 -0
- package/lib/cli.mjs +42 -1
- package/lib/self-update.mjs +553 -0
- package/package.json +1 -1
- package/scripts/lib/manifest.mjs +29 -0
- package/skills/engineering/implement-task/SKILL.md +124 -0
- package/skills/engineering/tdd/SKILL.md +2 -0
- package/skills/planning/create-task/REFERENCE.md +15 -6
- package/skills/planning/create-task/SKILL.md +1 -1
- package/skills/planning/create-task/pipeline.json +8 -0
- package/skills/planning/create-task/scripts/create-chores.mjs +27 -18
- package/skills/planning/create-task/templates/code.md +8 -3
- package/skills/planning/create-task/templates/document.md +8 -3
- package/skills/planning/create-task/templates/fix.md +8 -3
- package/skills/planning/create-task/templates/housekeeping.md +8 -4
- package/skills/planning/create-task/templates/post-merge.md +67 -0
- package/skills/planning/create-task/templates/review.md +8 -3
- package/skills/planning/create-task/templates/verify.md +8 -3
- package/skills/productivity/bd-tool/SKILL.md +55 -0
- package/skills/review/review-defects/EXAMPLES.md +198 -0
- package/skills/review/review-defects/SKILL.md +121 -0
- package/skills/review/review-report/EXAMPLES.md +114 -0
- package/skills/review/review-report/SKILL.md +138 -0
- package/skills/review/review-report/scripts/report.mjs +1032 -0
- package/skills/style/style-tests/EXAMPLES.md +30 -0
- package/skills/style/style-tests/SKILL.md +10 -6
- package/skills/workflow/hitl-queue/SKILL.md +134 -0
- package/skills/workflow/hitl-queue/scripts/queue.mjs +626 -0
- package/skills/workflow/plan/SKILL.md +27 -3
- package/skills/workflow/prioritise-work/CHALLENGE.md +139 -0
- package/skills/workflow/prioritise-work/SKILL.md +150 -0
- package/skills/workflow/prioritise-work/scripts/challenge.mjs +505 -0
- package/skills/workflow/prioritise-work/scripts/strategy.mjs +566 -0
- package/skills/workflow/prioritise-work/templates/strategy.md +61 -0
- package/skills/workflow/ralph/REFERENCE.md +124 -27
- package/skills/workflow/ralph/SKILL.md +24 -15
- package/skills/workflow/ralph/scripts/post-merge-check.mjs +651 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +37 -22
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +14 -8
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +2 -2
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -2
- package/skills/workflow/run-pipeline-stage/playbooks/housekeeping.md +1 -1
- package/skills/workflow/run-pipeline-stage/playbooks/post-merge.md +54 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +37 -3
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +2 -2
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +55 -11
- package/skills/workflow/run-pipeline-stage/scripts/recover-brief.mjs +805 -0
- package/skills/workflow/run-pipeline-stage/scripts/trail-entry.mjs +702 -0
- package/skills/workflow/score-bead/FORMAT.md +73 -0
- package/skills/workflow/score-bead/SKILL.md +112 -0
- package/skills/workflow/score-bead/scripts/rice.mjs +664 -0
- package/skills/workflow/using-agent-skills/SKILL.md +21 -8
- package/token-map.README.md +27 -0
- package/token-map.json +16 -4
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { createTempRoot } from "../../test/helpers/temp-root.mjs";
|
|
8
|
+
import postMergeWatchExtension, { CHECK_SCRIPT_ENV, WATCH_FLAG, checkScript } from "./index.ts";
|
|
9
|
+
|
|
10
|
+
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const tempRoot = createTempRoot("post-merge-watch-index-");
|
|
13
|
+
|
|
14
|
+
interface FlagRegistration {
|
|
15
|
+
name: string;
|
|
16
|
+
options: { description?: string; type: "string" | "boolean"; default?: string | boolean };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SentMessage {
|
|
20
|
+
message: { customType: string; content: string | unknown[]; display: boolean; details?: unknown };
|
|
21
|
+
options: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" } | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ExecCall {
|
|
25
|
+
command: string;
|
|
26
|
+
args: string[];
|
|
27
|
+
options: { cwd?: string; timeout?: number } | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** An event handler as `pi.on` recorded it, called through `Reflect.apply`. */
|
|
31
|
+
type RecordedHandler = (...args: never[]) => unknown;
|
|
32
|
+
|
|
33
|
+
interface FakePi {
|
|
34
|
+
pi: ExtensionAPI;
|
|
35
|
+
flags: FlagRegistration[];
|
|
36
|
+
sent: SentMessage[];
|
|
37
|
+
execs: ExecCall[];
|
|
38
|
+
notifications: Array<{ message: string; type: string | undefined }>;
|
|
39
|
+
/** Fire every handler pi registered for `event`. */
|
|
40
|
+
fire(event: string, payload: unknown, context: unknown): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A double for the five `ExtensionAPI` members this extension reaches.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately partial: the complete-literal guard is
|
|
47
|
+
* `extensions/agent-modes/index.test.ts`, whose fake fails `tsc` when pi's
|
|
48
|
+
* interface grows a member.
|
|
49
|
+
*/
|
|
50
|
+
function createFakePi(options: {
|
|
51
|
+
flag: string | undefined;
|
|
52
|
+
scriptExitCode: number;
|
|
53
|
+
scriptStdout?: string;
|
|
54
|
+
}): FakePi {
|
|
55
|
+
const flags: FlagRegistration[] = [];
|
|
56
|
+
const sent: SentMessage[] = [];
|
|
57
|
+
const execs: ExecCall[] = [];
|
|
58
|
+
const notifications: Array<{ message: string; type: string | undefined }> = [];
|
|
59
|
+
const handlers = new Map<string, RecordedHandler[]>();
|
|
60
|
+
|
|
61
|
+
const pi = {
|
|
62
|
+
registerFlag: (name: string, registration: FlagRegistration["options"]) => {
|
|
63
|
+
flags.push({ name, options: registration });
|
|
64
|
+
},
|
|
65
|
+
getFlag: (name: string) => (name === WATCH_FLAG ? options.flag : undefined),
|
|
66
|
+
on: (event: string, handler: RecordedHandler) => {
|
|
67
|
+
handlers.set(event, [...(handlers.get(event) ?? []), handler]);
|
|
68
|
+
return () => {};
|
|
69
|
+
},
|
|
70
|
+
exec: async (command: string, args: string[], execOptions: ExecCall["options"]) => {
|
|
71
|
+
execs.push({ command, args, options: execOptions });
|
|
72
|
+
return {
|
|
73
|
+
stdout: options.scriptStdout ?? "",
|
|
74
|
+
stderr: "",
|
|
75
|
+
code: options.scriptExitCode,
|
|
76
|
+
killed: false,
|
|
77
|
+
truncated: false,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
sendMessage: (message: SentMessage["message"], sendOptions?: SentMessage["options"]) => {
|
|
81
|
+
sent.push({ message, options: sendOptions });
|
|
82
|
+
},
|
|
83
|
+
} as unknown as ExtensionAPI;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
pi,
|
|
87
|
+
flags,
|
|
88
|
+
sent,
|
|
89
|
+
execs,
|
|
90
|
+
notifications,
|
|
91
|
+
async fire(event, payload, context) {
|
|
92
|
+
for (const handler of handlers.get(event) ?? []) {
|
|
93
|
+
await Reflect.apply(handler, undefined, [
|
|
94
|
+
payload,
|
|
95
|
+
{
|
|
96
|
+
cwd: "/projects/one",
|
|
97
|
+
ui: { notify: (message: string, type?: string) => notifications.push({ message, type }) },
|
|
98
|
+
...(context as object),
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The startup event pi sends before a session is usable. */
|
|
107
|
+
const SESSION_START = { type: "session_start", reason: "startup" } as const;
|
|
108
|
+
|
|
109
|
+
/** Run one session start with `HOME` pointed at a temp root, so no test reads the real home. */
|
|
110
|
+
async function withTempHome<T>(run: (home: string) => Promise<T>): Promise<T> {
|
|
111
|
+
const home = await tempRoot.createWorkspace();
|
|
112
|
+
const previousHome = process.env.HOME;
|
|
113
|
+
process.env.HOME = home;
|
|
114
|
+
try {
|
|
115
|
+
return await run(home);
|
|
116
|
+
} finally {
|
|
117
|
+
process.env.HOME = previousHome;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The state directory the extension uses for a given home. */
|
|
122
|
+
function stateDirFor(home: string): string {
|
|
123
|
+
return join(home, ".pi", "agent-cortex", "post-merge-watch");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function exists(path: string): Promise<boolean> {
|
|
127
|
+
try {
|
|
128
|
+
await stat(path);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Flag registration ───────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
describe("post-merge-watch extension", () => {
|
|
138
|
+
it("registers its CLI flag in the factory, before any session starts", async () => {
|
|
139
|
+
const fake = createFakePi({ flag: undefined, scriptExitCode: 2 });
|
|
140
|
+
|
|
141
|
+
postMergeWatchExtension(fake.pi);
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(
|
|
144
|
+
fake.flags.map((flag) => ({ name: flag.name, type: flag.options.type })),
|
|
145
|
+
[{ name: "post-merge-watch", type: "string" }],
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("resolves the shipped check script without a hardcoded absolute path", () => {
|
|
150
|
+
const previous = process.env[CHECK_SCRIPT_ENV];
|
|
151
|
+
delete process.env[CHECK_SCRIPT_ENV];
|
|
152
|
+
try {
|
|
153
|
+
assert.match(checkScript(), /skills[/\\]workflow[/\\]ralph[/\\]scripts[/\\]post-merge-check\.mjs$/);
|
|
154
|
+
} finally {
|
|
155
|
+
if (previous !== undefined) process.env[CHECK_SCRIPT_ENV] = previous;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("polls the script the environment names, so an install can be pointed at a stub", async () => {
|
|
160
|
+
const previous = process.env[CHECK_SCRIPT_ENV];
|
|
161
|
+
process.env[CHECK_SCRIPT_ENV] = "/stub/post-merge-check.mjs";
|
|
162
|
+
try {
|
|
163
|
+
assert.equal(checkScript(), "/stub/post-merge-check.mjs");
|
|
164
|
+
} finally {
|
|
165
|
+
if (previous === undefined) delete process.env[CHECK_SCRIPT_ENV];
|
|
166
|
+
else process.env[CHECK_SCRIPT_ENV] = previous;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ─── Session lifecycle ─────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
it("starts one watch at session start and wakes the session when the check settles", async () => {
|
|
173
|
+
await withTempHome(async (home) => {
|
|
174
|
+
const fake = createFakePi({
|
|
175
|
+
flag: "agnt-ctx-86u4.2",
|
|
176
|
+
scriptExitCode: 1,
|
|
177
|
+
scriptStdout: JSON.stringify({ reason: "run-failed", runs: [{ url: "https://gh/run/9" }] }),
|
|
178
|
+
});
|
|
179
|
+
postMergeWatchExtension(fake.pi);
|
|
180
|
+
|
|
181
|
+
await fake.fire("session_start", SESSION_START, {});
|
|
182
|
+
|
|
183
|
+
assert.equal(fake.execs.length, 1);
|
|
184
|
+
assert.deepEqual(
|
|
185
|
+
{ args: fake.execs[0].args.slice(1), cwd: fake.execs[0].options?.cwd },
|
|
186
|
+
{ args: ["--wait", "0"], cwd: "/projects/one" },
|
|
187
|
+
);
|
|
188
|
+
assert.equal(fake.sent.length, 1);
|
|
189
|
+
assert.deepEqual(fake.sent[0].options, { deliverAs: "followUp", triggerTurn: true });
|
|
190
|
+
assert.match(fake.sent[0].message.content as string, /agnt-ctx-86u4\.2/);
|
|
191
|
+
assert.deepEqual(fake.sent[0].message.details, {
|
|
192
|
+
bead: "agnt-ctx-86u4.2",
|
|
193
|
+
verdict: "failed",
|
|
194
|
+
runUrl: "https://gh/run/9",
|
|
195
|
+
exitCode: 1,
|
|
196
|
+
reason: "run-failed",
|
|
197
|
+
attempts: 1,
|
|
198
|
+
project: "/projects/one",
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("starts no watch when the flag is absent", async () => {
|
|
204
|
+
await withTempHome(async (home) => {
|
|
205
|
+
const fake = createFakePi({ flag: undefined, scriptExitCode: 1 });
|
|
206
|
+
postMergeWatchExtension(fake.pi);
|
|
207
|
+
|
|
208
|
+
await fake.fire("session_start", SESSION_START, {});
|
|
209
|
+
|
|
210
|
+
assert.deepEqual(fake.execs, []);
|
|
211
|
+
assert.equal(await exists(stateDirFor(home)), false);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("records the watch under the agent-cortex state directory, then clears it on session shutdown", async () => {
|
|
216
|
+
await withTempHome(async (home) => {
|
|
217
|
+
const fake = createFakePi({ flag: "agnt-ctx-86u4.2", scriptExitCode: 2 });
|
|
218
|
+
postMergeWatchExtension(fake.pi);
|
|
219
|
+
|
|
220
|
+
await fake.fire("session_start", SESSION_START, {});
|
|
221
|
+
|
|
222
|
+
assert.deepEqual(
|
|
223
|
+
(await readdir(stateDirFor(home))).length,
|
|
224
|
+
1,
|
|
225
|
+
"the in-flight watch is recorded under ~/.pi/agent-cortex",
|
|
226
|
+
);
|
|
227
|
+
assert.equal(fake.sent.length, 0, "no verdict yet, so no wake");
|
|
228
|
+
|
|
229
|
+
await fake.fire("session_shutdown", { type: "session_shutdown", reason: "quit" }, {});
|
|
230
|
+
|
|
231
|
+
assert.deepEqual(await readdir(stateDirFor(home)), [], "an abandoned watch leaves no state file");
|
|
232
|
+
assert.deepEqual(fake.sent, [], "an abandoned watch wakes nobody");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("starts no second watch when the session reloads for the same bead", async () => {
|
|
237
|
+
await withTempHome(async () => {
|
|
238
|
+
const fake = createFakePi({ flag: "agnt-ctx-86u4.2", scriptExitCode: 2 });
|
|
239
|
+
postMergeWatchExtension(fake.pi);
|
|
240
|
+
|
|
241
|
+
await fake.fire("session_start", SESSION_START, {});
|
|
242
|
+
await fake.fire("session_start", { type: "session_start", reason: "reload" }, {});
|
|
243
|
+
|
|
244
|
+
assert.equal(fake.execs.length, 1);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-merge watch extension.
|
|
3
|
+
*
|
|
4
|
+
* A human merges the feature PR, and the CI run for the merge commit decides
|
|
5
|
+
* what happens next. Nothing tells the agent when that run concludes: the merge
|
|
6
|
+
* happens outside the session, and by the time the run settles the agent is idle.
|
|
7
|
+
* This extension makes the session wake up with the verdict.
|
|
8
|
+
*
|
|
9
|
+
* It polls the deterministic check script
|
|
10
|
+
* (`skills/workflow/ralph/scripts/post-merge-check.mjs`) on a timer and, when the
|
|
11
|
+
* script reports a verdict, sends one follow-up message — `deliverAs:
|
|
12
|
+
* "followUp"`, `triggerTurn: true` — so an idle session starts a turn with the
|
|
13
|
+
* result. The script owns the verdict; this extension runs it and forwards the
|
|
14
|
+
* exit code. It makes no model call.
|
|
15
|
+
*
|
|
16
|
+
* Opt-in per session through the `--post-merge-watch <bead>` flag. State lives
|
|
17
|
+
* under `~/.pi/agent-cortex/post-merge-watch/`, one file per project and bead,
|
|
18
|
+
* and is removed when the watch settles or the session shuts down.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
|
|
26
|
+
import { PostMergeWatch, DEFAULT_INTERVAL_MS, DEFAULT_MAX_ATTEMPTS } from "./watch.ts";
|
|
27
|
+
import type { CheckRun, CheckInvocation, WakeMessage } from "./watch.ts";
|
|
28
|
+
|
|
29
|
+
/** The CLI flag that starts a watch: `--post-merge-watch <bead>`. */
|
|
30
|
+
export const WATCH_FLAG = "post-merge-watch";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Environment override for the check script path. The default resolves against
|
|
34
|
+
* this module, so the staged pi package (`extensions/` beside `skills/`) and a
|
|
35
|
+
* checkout of this repository both find the shipped script without a hardcoded
|
|
36
|
+
* absolute path.
|
|
37
|
+
*/
|
|
38
|
+
export const CHECK_SCRIPT_ENV = "AGENT_CORTEX_POST_MERGE_CHECK";
|
|
39
|
+
|
|
40
|
+
/** The check script's location in a staged install and in this repository. */
|
|
41
|
+
const DEFAULT_CHECK_SCRIPT = new URL(
|
|
42
|
+
"../../skills/workflow/ralph/scripts/post-merge-check.mjs",
|
|
43
|
+
import.meta.url,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/** How long one check invocation may take before pi kills it. */
|
|
47
|
+
const CHECK_TIMEOUT_MS = 120_000;
|
|
48
|
+
|
|
49
|
+
/** The state directory, under the agent-cortex global area, never in the project. */
|
|
50
|
+
export function stateDir(): string {
|
|
51
|
+
return join(homedir(), ".pi", "agent-cortex", "post-merge-watch");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The check script this extension polls. */
|
|
55
|
+
export function checkScript(): string {
|
|
56
|
+
const override = process.env[CHECK_SCRIPT_ENV];
|
|
57
|
+
return override !== undefined && override !== "" ? override : fileURLToPath(DEFAULT_CHECK_SCRIPT);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default function postMergeWatchExtension(pi: ExtensionAPI): void {
|
|
61
|
+
// Registered here, in the factory: pi matches parsed CLI flags against the
|
|
62
|
+
// extensions loaded at startup, so a flag registered from `session_start` is
|
|
63
|
+
// rejected as an unknown option.
|
|
64
|
+
pi.registerFlag(WATCH_FLAG, {
|
|
65
|
+
description: "Watch the post-merge check for a bead and wake the session with the verdict",
|
|
66
|
+
type: "string",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** One watch per bead, so a reloaded session does not start a second one. */
|
|
70
|
+
const watches = new Map<string, PostMergeWatch>();
|
|
71
|
+
|
|
72
|
+
const runCheck = async (invocation: CheckInvocation): Promise<CheckRun> => {
|
|
73
|
+
const result = await pi.exec(process.execPath, [invocation.script, ...invocation.args], {
|
|
74
|
+
cwd: invocation.cwd,
|
|
75
|
+
timeout: CHECK_TIMEOUT_MS,
|
|
76
|
+
});
|
|
77
|
+
return { code: result.code, stdout: result.stdout, stderr: result.stderr, killed: result.killed };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The verdict goes out as a follow-up message with `triggerTurn`, so a session
|
|
82
|
+
* that is idle starts a turn and one that is busy reads it after the turn in
|
|
83
|
+
* progress instead of interrupting it.
|
|
84
|
+
*/
|
|
85
|
+
const deliver = (message: WakeMessage): void => {
|
|
86
|
+
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const watchedBead = (): string | undefined => {
|
|
90
|
+
const value = pi.getFlag(WATCH_FLAG);
|
|
91
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
95
|
+
for (const [bead, watch] of watches) {
|
|
96
|
+
if (!watch.active) watches.delete(bead);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const bead = watchedBead();
|
|
100
|
+
if (bead === undefined || watches.has(bead)) return;
|
|
101
|
+
|
|
102
|
+
const watch = new PostMergeWatch({
|
|
103
|
+
bead,
|
|
104
|
+
project: ctx.cwd,
|
|
105
|
+
script: checkScript(),
|
|
106
|
+
stateDir: stateDir(),
|
|
107
|
+
runCheck,
|
|
108
|
+
deliver,
|
|
109
|
+
intervalMs: DEFAULT_INTERVAL_MS,
|
|
110
|
+
maxAttempts: DEFAULT_MAX_ATTEMPTS,
|
|
111
|
+
warn: (message) => ctx.ui.notify(message, "warning"),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
watches.set(bead, watch);
|
|
115
|
+
await watch.start();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
pi.on("session_shutdown", async () => {
|
|
119
|
+
const stopping = [...watches.values()].map((watch) => watch.stop());
|
|
120
|
+
watches.clear();
|
|
121
|
+
await Promise.all(stopping);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-cortex/post-merge-watch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test --import tsx/esm watch.test.ts index.test.ts",
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@earendil-works/pi-coding-agent": "^1.5.2",
|
|
12
|
+
"@types/node": "^22.0.0",
|
|
13
|
+
"tsx": "^4.0.0",
|
|
14
|
+
"typescript": "^5.0.0"
|
|
15
|
+
}
|
|
16
|
+
}
|