@jaybeeuu/agent-cortex 2.3.0 → 2.4.1
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 +61 -0
- package/README.md +25 -6
- package/bin/installers/claude.mjs +108 -5
- package/bin/installers/pi.mjs +176 -18
- package/extensions/agent-guardrails/package.json +0 -3
- package/extensions/agent-modes/index.ts +4 -3
- package/extensions/agent-modes/package.json +1 -5
- package/extensions/auto-name.ts +16 -49
- package/extensions/lib/tiny-model.ts +15 -151
- package/extensions/notify/index.ts +9 -59
- package/extensions/output-style/package.json +0 -3
- package/extensions/session-start/package.json +0 -3
- package/extensions/skill-stats/package.json +0 -3
- package/extensions/subagent/README.md +2 -2
- package/extensions/subagent/agents.ts +17 -1
- package/extensions/subagent/components.test.ts +111 -0
- package/extensions/subagent/components.ts +182 -0
- package/extensions/subagent/frontmatter.test.ts +74 -0
- package/extensions/subagent/frontmatter.ts +105 -0
- package/extensions/subagent/index.ts +14 -12
- package/extensions/subagent/package.json +3 -4
- package/package.json +1 -1
- package/skills/planning/create-task/templates/review.md +13 -8
- package/skills/review/review-security/SKILL.md +4 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript components for the subagent tool's renderers.
|
|
3
|
+
*
|
|
4
|
+
* pi-tui's `Text`, `Container` and `Spacer` are not reachable from an installed
|
|
5
|
+
* extension: a value import of `@earendil-works/pi-tui` makes the extension fail
|
|
6
|
+
* to load outside the agent-cortex checkout (test/pi-extensions-load.test.mjs).
|
|
7
|
+
* These are the local equivalents, implementing the `Component` contract pi's
|
|
8
|
+
* transcript renderer calls: `render(width)` returns lines that fit `width`, and
|
|
9
|
+
* `invalidate()` drops cached layout.
|
|
10
|
+
*
|
|
11
|
+
* Two deliberate limits, because reimplementing pi-tui is not this change:
|
|
12
|
+
* markdown is drawn as plain text, and only SGR colour sequences (`ESC[…m`) are
|
|
13
|
+
* measured and carried across a wrapped line. Other escapes — OSC-8 hyperlinks,
|
|
14
|
+
* images — count as visible characters, the same way a naive strip does.
|
|
15
|
+
* Width is measured in terminal cells: a wide (East Asian) character, and any
|
|
16
|
+
* astral character such as an emoji, counts as two.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Lines to draw at a given width — what pi's transcript renderer calls. */
|
|
20
|
+
export interface Component {
|
|
21
|
+
render(width: number): string[];
|
|
22
|
+
invalidate(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SGR = /\x1b\[[0-9;]*m/g;
|
|
26
|
+
const SGR_AT = /\x1b\[[0-9;]*m/y;
|
|
27
|
+
const RESET = "\x1b[0m";
|
|
28
|
+
|
|
29
|
+
/** A block of text, word-wrapped to the width it is drawn at. */
|
|
30
|
+
export class Text implements Component {
|
|
31
|
+
readonly #text: string;
|
|
32
|
+
readonly #paddingX: number;
|
|
33
|
+
readonly #paddingY: number;
|
|
34
|
+
|
|
35
|
+
constructor(text = "", paddingX = 0, paddingY = 0) {
|
|
36
|
+
this.#text = text;
|
|
37
|
+
this.#paddingX = paddingX;
|
|
38
|
+
this.#paddingY = paddingY;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
render(width: number): string[] {
|
|
42
|
+
const available = Math.max(1, width - this.#paddingX * 2);
|
|
43
|
+
const indent = " ".repeat(this.#paddingX);
|
|
44
|
+
const blank = indent.repeat(2);
|
|
45
|
+
const body = this.#text.split("\n").flatMap((line) => wrapLine(line, available));
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
...emptyLines(this.#paddingY, blank),
|
|
49
|
+
...body.map((line) => `${indent}${line}${indent}`),
|
|
50
|
+
...emptyLines(this.#paddingY, blank),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
invalidate(): void {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Blank lines. */
|
|
58
|
+
export class Spacer implements Component {
|
|
59
|
+
readonly #lines: number;
|
|
60
|
+
|
|
61
|
+
constructor(lines = 1) {
|
|
62
|
+
this.#lines = lines;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render(_width: number): string[] {
|
|
66
|
+
return emptyLines(this.#lines, "");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
invalidate(): void {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A vertical stack of components. */
|
|
73
|
+
export class Container implements Component {
|
|
74
|
+
readonly #children: Component[] = [];
|
|
75
|
+
|
|
76
|
+
addChild(child: Component): void {
|
|
77
|
+
this.#children.push(child);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
render(width: number): string[] {
|
|
81
|
+
return this.#children.flatMap((child) => child.render(width));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
invalidate(): void {
|
|
85
|
+
for (const child of this.#children) child.invalidate();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `count` blank lines, each filled with `filler` (padding keeps the width). */
|
|
90
|
+
function emptyLines(count: number, filler: string): string[] {
|
|
91
|
+
return Array.from({ length: Math.max(0, count) }, () => filler);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Visible cell count of a line: everything except SGR sequences, wide characters as two. */
|
|
95
|
+
function visibleWidth(line: string): number {
|
|
96
|
+
let cells = 0;
|
|
97
|
+
for (const character of line.replace(SGR, "")) cells += cellWidth(character.codePointAt(0) ?? 0);
|
|
98
|
+
return cells;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Terminal cells one character occupies. Wide East Asian characters and astral
|
|
103
|
+
* characters (emoji included) occupy two; every other character occupies one.
|
|
104
|
+
* Counting in `String.length` units would let a wide character overflow: it is
|
|
105
|
+
* one unit but two cells.
|
|
106
|
+
*/
|
|
107
|
+
function cellWidth(codePoint: number): number {
|
|
108
|
+
if (codePoint > 0xffff) return 2;
|
|
109
|
+
return codePoint >= 0x1100 &&
|
|
110
|
+
(codePoint <= 0x115f ||
|
|
111
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
|
|
112
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
113
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
114
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
115
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
116
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6))
|
|
117
|
+
? 2
|
|
118
|
+
: 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Wrap one line to `width` visible columns without cutting an SGR sequence. A
|
|
123
|
+
* cut inside a coloured span closes the line and reopens the same colour on the
|
|
124
|
+
* continuation, so broken escapes never reach the terminal.
|
|
125
|
+
*/
|
|
126
|
+
function wrapLine(line: string, width: number): string[] {
|
|
127
|
+
const lines: string[] = [];
|
|
128
|
+
let rest = line;
|
|
129
|
+
|
|
130
|
+
while (visibleWidth(rest) > width) {
|
|
131
|
+
const cut = breakIndex(rest, width);
|
|
132
|
+
if (cut <= 0) break;
|
|
133
|
+
|
|
134
|
+
const head = rest.slice(0, cut);
|
|
135
|
+
const colour = lastColour(head);
|
|
136
|
+
lines.push(colour === "" ? head : `${head}${RESET}`);
|
|
137
|
+
// A break taken at a space consumes it, so the continuation starts with its word.
|
|
138
|
+
rest = `${colour}${rest.slice(rest[cut] === " " ? cut + 1 : cut)}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push(rest);
|
|
142
|
+
return lines;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Index at which to cut `text` so its visible width stays within `width`:
|
|
147
|
+
* the last space at or before the limit, or the limit itself when the line has
|
|
148
|
+
* no usable space.
|
|
149
|
+
*/
|
|
150
|
+
function breakIndex(text: string, width: number): number {
|
|
151
|
+
let visible = 0;
|
|
152
|
+
let lastSpace = -1;
|
|
153
|
+
let index = 0;
|
|
154
|
+
|
|
155
|
+
while (index < text.length) {
|
|
156
|
+
SGR_AT.lastIndex = index;
|
|
157
|
+
const escape = SGR_AT.exec(text);
|
|
158
|
+
if (escape !== null) {
|
|
159
|
+
index += escape[0].length;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const codePoint = text.codePointAt(index) ?? 0;
|
|
164
|
+
const character = String.fromCodePoint(codePoint);
|
|
165
|
+
if (character === " ") lastSpace = index;
|
|
166
|
+
if (visible + cellWidth(codePoint) > width) return lastSpace > 0 ? lastSpace : index;
|
|
167
|
+
|
|
168
|
+
visible += cellWidth(codePoint);
|
|
169
|
+
index += character.length;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return lastSpace > 0 ? lastSpace : index;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The colour sequence left open at the end of `text`, or "" when none is. */
|
|
176
|
+
function lastColour(text: string): string {
|
|
177
|
+
let colour = "";
|
|
178
|
+
for (const [sequence] of text.matchAll(SGR)) {
|
|
179
|
+
colour = sequence === RESET || sequence === "\x1b[m" ? "" : sequence;
|
|
180
|
+
}
|
|
181
|
+
return colour;
|
|
182
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local frontmatter reader for agent files.
|
|
3
|
+
*
|
|
4
|
+
* pi's `parseFrontmatter` is unreachable from an installed extension, so this
|
|
5
|
+
* parser reads the subset agent files use. Agents are user-authored files at a
|
|
6
|
+
* system edge: a field this parser mishandles silently changes an agent's name,
|
|
7
|
+
* description or tool list, and an exception here would break discovery of every
|
|
8
|
+
* other agent, so both the parsing and the tolerance are covered.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
|
|
14
|
+
import { parseFrontmatter } from "./frontmatter.ts";
|
|
15
|
+
|
|
16
|
+
describe("parseFrontmatter", () => {
|
|
17
|
+
it("splits fields from the body and trims the body", () => {
|
|
18
|
+
const { frontmatter, body } = parseFrontmatter(`---\nname: ralph\ndescription: d\n---\n\n# ralph\n`);
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(frontmatter, { name: "ralph", description: "d" });
|
|
21
|
+
assert.equal(body, "# ralph");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("reads a quoted scalar, unescaping it", () => {
|
|
25
|
+
const { frontmatter } = parseFrontmatter(`---\nname: "a: b\\"c\\n"\n---\n`);
|
|
26
|
+
|
|
27
|
+
assert.equal(frontmatter.name, 'a: b"c\n');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("reads a flow sequence of quoted items", () => {
|
|
31
|
+
const { frontmatter } = parseFrontmatter(`---\ntools: ["bash", "read"]\n---\n`);
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(frontmatter.tools, ["bash", "read"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("reads a block sequence", () => {
|
|
37
|
+
const { frontmatter } = parseFrontmatter(`---\ntools:\n - bash\n - read\n---\n`);
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(frontmatter.tools, ["bash", "read"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("keeps a comma-separated tool list as one scalar", () => {
|
|
43
|
+
const { frontmatter } = parseFrontmatter(`---\ntools: "bash, read"\n---\n`);
|
|
44
|
+
|
|
45
|
+
assert.equal(frontmatter.tools, "bash, read");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("separates fields from a following block sequence", () => {
|
|
49
|
+
const { frontmatter } = parseFrontmatter(`---\nname: ralph\ntools:\n - bash\nmodel: fast\n---\n`);
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(frontmatter, { name: "ralph", tools: ["bash"], model: "fast" });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("normalises CRLF line endings", () => {
|
|
55
|
+
const { frontmatter, body } = parseFrontmatter(`---\r\nname: ralph\r\n---\r\n\r\nbody\r\n`);
|
|
56
|
+
|
|
57
|
+
assert.deepEqual(frontmatter, { name: "ralph" });
|
|
58
|
+
assert.equal(body, "body");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns the whole document as body when there is no frontmatter", () => {
|
|
62
|
+
const { frontmatter, body } = parseFrontmatter("# just markdown\n");
|
|
63
|
+
|
|
64
|
+
assert.deepEqual(frontmatter, {});
|
|
65
|
+
assert.equal(body, "# just markdown\n");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("treats an unclosed block as body rather than throwing", () => {
|
|
69
|
+
const { frontmatter, body } = parseFrontmatter(`---\nname: ralph\n`);
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(frontmatter, {});
|
|
72
|
+
assert.equal(body, "---\nname: ralph\n");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the YAML frontmatter block of a markdown document.
|
|
3
|
+
*
|
|
4
|
+
* pi's own `parseFrontmatter` is not reachable from an installed extension: a
|
|
5
|
+
* value import of `@earendil-works/pi-coding-agent` makes the extension fail to
|
|
6
|
+
* load outside the agent-cortex checkout (test/pi-extensions-load.test.mjs).
|
|
7
|
+
* This module is the subset agent files use — top-level `key: value` pairs,
|
|
8
|
+
* plain and quoted scalars, flow sequences (`[a, b]`) and block sequences
|
|
9
|
+
* (`- a`).
|
|
10
|
+
*
|
|
11
|
+
* Deliberately outside the subset: nested maps and block scalars (`|`, `>`) keep
|
|
12
|
+
* their literal text as the value. No construct throws — an unreadable block
|
|
13
|
+
* yields empty frontmatter — so one malformed agent file cannot break discovery
|
|
14
|
+
* of the others.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A parsed document: its frontmatter fields and the markdown body. */
|
|
18
|
+
export interface ParsedFrontmatter<T extends Record<string, unknown>> {
|
|
19
|
+
frontmatter: T;
|
|
20
|
+
body: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FENCE = "---";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Split a markdown document into frontmatter fields and body, matching pi's
|
|
27
|
+
* contract: the document must open with `---` and close the block with a line
|
|
28
|
+
* that starts with `---`; the body is trimmed.
|
|
29
|
+
*/
|
|
30
|
+
export function parseFrontmatter<T extends Record<string, unknown>>(content: string): ParsedFrontmatter<T> {
|
|
31
|
+
const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
|
32
|
+
const end = normalized.startsWith(FENCE) ? normalized.indexOf(`\n${FENCE}`, FENCE.length) : -1;
|
|
33
|
+
if (end === -1) return { frontmatter: {} as T, body: normalized };
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
frontmatter: parseBlock(normalized.slice(FENCE.length + 1, end)) as T,
|
|
37
|
+
body: normalized.slice(end + FENCE.length + 1).trim(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Parse the `key: value` lines of a frontmatter block. */
|
|
42
|
+
function parseBlock(block: string): Record<string, unknown> {
|
|
43
|
+
const fields: Record<string, unknown> = {};
|
|
44
|
+
const lines = block.split("\n");
|
|
45
|
+
|
|
46
|
+
for (let index = 0; index < lines.length; index++) {
|
|
47
|
+
const match = /^([\w.-]+):(?:[ \t]+(.*))?$/.exec(lines[index]);
|
|
48
|
+
if (match === null) continue;
|
|
49
|
+
|
|
50
|
+
const [, key, inlineValue] = match;
|
|
51
|
+
if (inlineValue !== undefined && inlineValue.trim() !== "") {
|
|
52
|
+
fields[key] = parseValue(inlineValue.trim());
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { items, consumed } = readBlockSequence(lines, index + 1);
|
|
57
|
+
index += consumed;
|
|
58
|
+
fields[key] = items.length > 0 ? items : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return fields;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read the indented `- item` lines that follow a `key:` line with no inline
|
|
66
|
+
* value. Returns the items and how many lines were consumed.
|
|
67
|
+
*/
|
|
68
|
+
function readBlockSequence(lines: string[], start: number): { items: string[]; consumed: number } {
|
|
69
|
+
const items: string[] = [];
|
|
70
|
+
let consumed = 0;
|
|
71
|
+
|
|
72
|
+
for (let index = start; index < lines.length; index++) {
|
|
73
|
+
const match = /^[ \t]+-[ \t]+(.*)$/.exec(lines[index]);
|
|
74
|
+
if (match === null) break;
|
|
75
|
+
|
|
76
|
+
const value = parseValue(match[1].trim());
|
|
77
|
+
items.push(typeof value === "string" ? value : String(value));
|
|
78
|
+
consumed++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { items, consumed };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Parse one scalar or flow sequence. */
|
|
85
|
+
function parseValue(raw: string): unknown {
|
|
86
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
87
|
+
return raw
|
|
88
|
+
.slice(1, -1)
|
|
89
|
+
.split(",")
|
|
90
|
+
.map((item) => item.trim())
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map(parseValue);
|
|
93
|
+
}
|
|
94
|
+
if (raw.startsWith('"')) return unquote(raw, '"', (text) => text.replace(/\\(["\\/nrt])/g, (_, escape: string) => {
|
|
95
|
+
return escape === "n" ? "\n" : escape === "r" ? "\r" : escape === "t" ? "\t" : escape;
|
|
96
|
+
}));
|
|
97
|
+
if (raw.startsWith("'")) return unquote(raw, "'", (text) => text.replace(/''/g, "'"));
|
|
98
|
+
return raw;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Strip a matching quote pair and unescape the content; an unterminated quote keeps the raw text. */
|
|
102
|
+
function unquote(raw: string, quote: string, unescape: (text: string) => string): string {
|
|
103
|
+
if (raw.length < 2 || !raw.endsWith(quote)) return raw;
|
|
104
|
+
return unescape(raw.slice(1, -1));
|
|
105
|
+
}
|
|
@@ -21,8 +21,13 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
21
21
|
import type { Message } from "@earendil-works/pi-ai";
|
|
22
22
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts";
|
|
23
23
|
import { waitForAgents } from "./wait.ts";
|
|
24
|
-
import {
|
|
25
|
-
import { Container,
|
|
24
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { Container, Spacer, Text } from "./components.ts";
|
|
26
|
+
// `typebox` is the one bare runtime import an extension may make: pi documents
|
|
27
|
+
// it as host-provided and its loader aliases it (pi's docs/packages.md), so it
|
|
28
|
+
// resolves in the staged package too. Every `@earendil-works/*` import here is
|
|
29
|
+
// `import type`, which is erased; a value import of pi's runtime breaks the
|
|
30
|
+
// package outside this checkout (test/pi-extensions-load.test.mjs).
|
|
26
31
|
import { Type } from "typebox";
|
|
27
32
|
|
|
28
33
|
|
|
@@ -236,9 +241,8 @@ async function writePromptToTempFile(agentName: string, prompt: string): Promise
|
|
|
236
241
|
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
|
|
237
242
|
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
238
243
|
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
});
|
|
244
|
+
// No mutation queue: the directory is fresh per call, so nothing else writes this file.
|
|
245
|
+
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
|
242
246
|
return { dir: tmpDir, filePath };
|
|
243
247
|
}
|
|
244
248
|
|
|
@@ -746,8 +750,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
746
750
|
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
747
751
|
}
|
|
748
752
|
|
|
749
|
-
const mdTheme = getMarkdownTheme();
|
|
750
|
-
|
|
751
753
|
const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
|
|
752
754
|
const toShow = limit ? items.slice(-limit) : items;
|
|
753
755
|
const skipped = limit && items.length > limit ? items.length - limit : 0;
|
|
@@ -798,7 +800,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
798
800
|
}
|
|
799
801
|
if (finalOutput) {
|
|
800
802
|
container.addChild(new Spacer(1));
|
|
801
|
-
container.addChild(new
|
|
803
|
+
container.addChild(new Text(finalOutput.trim(), 0, 0));
|
|
802
804
|
}
|
|
803
805
|
}
|
|
804
806
|
const usageStr = formatUsageStats(r.usage, r.model);
|
|
@@ -880,10 +882,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
880
882
|
}
|
|
881
883
|
}
|
|
882
884
|
|
|
883
|
-
// Show final output as
|
|
885
|
+
// Show final output as plain text
|
|
884
886
|
if (finalOutput) {
|
|
885
887
|
container.addChild(new Spacer(1));
|
|
886
|
-
container.addChild(new
|
|
888
|
+
container.addChild(new Text(finalOutput.trim(), 0, 0));
|
|
887
889
|
}
|
|
888
890
|
|
|
889
891
|
const stepUsage = formatUsageStats(r.usage, r.model);
|
|
@@ -965,10 +967,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
965
967
|
}
|
|
966
968
|
}
|
|
967
969
|
|
|
968
|
-
// Show final output as
|
|
970
|
+
// Show final output as plain text
|
|
969
971
|
if (finalOutput) {
|
|
970
972
|
container.addChild(new Spacer(1));
|
|
971
|
-
container.addChild(new
|
|
973
|
+
container.addChild(new Text(finalOutput.trim(), 0, 0));
|
|
972
974
|
}
|
|
973
975
|
|
|
974
976
|
const taskUsage = formatUsageStats(r.usage, r.model);
|
|
@@ -4,20 +4,19 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --test --import tsx/esm wait.test.ts agents.test.ts",
|
|
7
|
+
"test": "node --test --import tsx/esm wait.test.ts agents.test.ts frontmatter.test.ts components.test.ts",
|
|
8
8
|
"typecheck": "tsc --noEmit"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@earendil-works/pi-agent-core": "^0.78.1",
|
|
12
12
|
"@earendil-works/pi-ai": "^0.78.1",
|
|
13
13
|
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
14
|
-
"@earendil-works/pi-tui": "^0.78.1",
|
|
15
14
|
"@types/node": "^22.0.0",
|
|
16
15
|
"tsx": "^4.0.0",
|
|
17
16
|
"typebox": "^1.1.38",
|
|
18
17
|
"typescript": "^5.0.0"
|
|
19
18
|
},
|
|
20
19
|
"peerDependencies": {
|
|
21
|
-
"
|
|
20
|
+
"typebox": "*"
|
|
22
21
|
}
|
|
23
|
-
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -16,21 +16,26 @@ Log stage start, security scan result, review outcome, and stage complete (see P
|
|
|
16
16
|
|
|
17
17
|
## Instructions
|
|
18
18
|
**First**, invoke the `review-security` skill and run the scan against the changes. If the
|
|
19
|
-
verdict is FAIL, set `
|
|
20
|
-
|
|
19
|
+
verdict is FAIL, set `OUTCOME: BLOCKED` immediately and list each finding under
|
|
20
|
+
`BLOCKING_ISSUES` — do not proceed with the quality review.
|
|
21
21
|
|
|
22
22
|
**Then**, review the implementation for correctness, quality, and alignment with the task
|
|
23
23
|
description and project conventions. Only flag genuine correctness issues or clear deviations
|
|
24
24
|
from stated requirements — not stylistic preferences.
|
|
25
25
|
|
|
26
|
-
End your response with a ---REPORT--- block
|
|
26
|
+
End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
|
|
27
|
+
`prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that
|
|
28
|
+
prompt differ, follow the prompt and correct this template.
|
|
27
29
|
---REPORT---
|
|
28
30
|
BEAD_ID: <id>
|
|
29
|
-
STAGE_COMPLETED:
|
|
31
|
+
STAGE_COMPLETED: review
|
|
30
32
|
SUMMARY: <2–3 sentence summary of what you reviewed>
|
|
31
33
|
FILES_CHANGED: none
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
COMMIT: <sha, or "none" if this stage added no commit>
|
|
35
|
+
PUSHED: <yes|no>
|
|
36
|
+
PR: none
|
|
37
|
+
OUTCOME: <SUCCESS|BLOCKED>
|
|
38
|
+
BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one numbered line per finding
|
|
39
|
+
1. <path:line — what is wrong and what to fix>
|
|
40
|
+
2. <path:line — what is wrong and what to fix>
|
|
36
41
|
---
|
|
@@ -54,7 +54,10 @@ bash "$(find ~ -path '*/agent-cortex/skills/review-security/scripts/install-gitl
|
|
|
54
54
|
|
|
55
55
|
If the verdict is FAIL, report each finding as a required change. Do not proceed until secrets are removed.
|
|
56
56
|
|
|
57
|
-
When running within a ralph review pipeline, the outcome is communicated through the
|
|
57
|
+
When running within a ralph review pipeline, the outcome is communicated through the
|
|
58
|
+
`---REPORT---` block that ralph parses — set `OUTCOME: BLOCKED` in that block and list each
|
|
59
|
+
finding under `BLOCKING_ISSUES` with its file and line (see the review template in the
|
|
60
|
+
`create-task` skill's `templates/review.md`).
|
|
58
61
|
|
|
59
62
|
## Red Flags
|
|
60
63
|
|