@jaybeeuu/agent-cortex 1.41.1 → 1.43.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/.agents/skills/style-versioning/SKILL.md +73 -2
- package/.github/workflows/ci.yml +52 -32
- package/AGENTS.md +23 -2
- package/CHANGELOG.md +189 -0
- package/README.md +125 -73
- package/agents/README.md +17 -4
- package/agents/ralph-plan/agent.md +7 -3
- package/agents/ralph-plan.agent.md +7 -3
- package/bin/agent-cortex.mjs +48 -30
- package/bin/installers/pi.mjs +885 -237
- package/claude-extras/scripts/statusline-command.sh +0 -0
- package/docs/claude-hooks.md +5 -4
- package/docs/commit-signing.md +231 -0
- package/docs/ideas/ban-content-assertion-tests.md +3 -1
- package/docs/ideas/dedicated-review-tests-skill.md +71 -0
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +10 -1
- package/docs/ideas/extension-manifests.md +5 -0
- package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +71 -0
- package/docs/research/disable-model-invocation.md +103 -0
- package/docs/research/skill-design-research.md +2 -2
- package/docs/skills/skill-anatomy.md +5 -1
- package/docs/technical-direction/harness-config.md +25 -1
- package/docs/technical-direction/migration-execution-labels.md +17 -9
- package/extensions/agent-modes/discover.test.ts +19 -0
- package/extensions/agent-modes/discover.ts +25 -3
- package/extensions/session-start/README.md +38 -0
- package/extensions/session-start/index.test.ts +122 -0
- package/extensions/session-start/index.ts +27 -0
- package/extensions/session-start/package.json +19 -0
- package/extensions/session-start/tsconfig.json +12 -0
- package/extensions/subagent/README.md +1 -1
- package/extensions/subagent/agents.test.ts +176 -0
- package/extensions/subagent/agents.ts +102 -11
- package/extensions/subagent/index.ts +2 -1
- package/extensions/subagent/package.json +1 -1
- package/lib/cli.mjs +35 -9
- package/package.json +10 -6
- package/pi/settings.json +1 -7
- package/plugin.json +1 -1
- package/pnpm-workspace.yaml +6 -0
- package/scripts/check-changeset.mjs +42 -0
- package/scripts/install-instructions.sh +0 -0
- package/scripts/lib/release-gates.mjs +61 -0
- package/scripts/lib/verify-worker-tree.mjs +107 -0
- package/scripts/resolve-publish-command.mjs +17 -0
- package/scripts/sync-plugin-version.sh +0 -0
- package/skills/engineering/grill-with-docs/SKILL.md +3 -2
- package/skills/engineering/improve-codebase-architecture/REFERENCE.md +5 -0
- package/skills/engineering/improve-codebase-architecture/SKILL.md +2 -1
- package/skills/planning/create-task/SKILL.md +8 -3
- package/skills/planning/create-task/scripts/create-chores.ts +4 -1
- package/skills/planning/prd-to-tasks/REFERENCE.md +2 -2
- package/skills/planning/prd-to-tasks/SKILL.md +9 -4
- package/skills/planning/request-refactor-plan/FORMAT.md +5 -0
- package/skills/planning/request-refactor-plan/SKILL.md +3 -2
- package/skills/planning/spec-to-tickets/REFERENCE.md +82 -0
- package/skills/planning/spec-to-tickets/SKILL.md +143 -0
- package/skills/planning/write-a-prd/FORMAT.md +6 -0
- package/skills/planning/write-a-prd/SKILL.md +9 -6
- package/skills/planning/write-a-ticket/EXAMPLES.md +133 -0
- package/skills/planning/write-a-ticket/SKILL.md +77 -59
- package/skills/productivity/bd-tool/SKILL.md +27 -2
- package/skills/productivity/grill-me/SKILL.md +11 -0
- package/skills/review/review-security/scripts/install-gitleaks.sh +0 -0
- package/skills/workflow/git-workflow/SKILL.md +35 -1
- package/skills/workflow/plan/SKILL.md +20 -8
- package/skills/workflow/ralph/REFERENCE.md +20 -2
- package/skills/workflow/ralph/SKILL.md +4 -1
- package/skills/workflow/run-pipeline-stage/SKILL.md +17 -5
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +2 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +3 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +11 -0
- package/skills/workflow/using-agent-skills/SKILL.md +1 -0
- package/test/cli.test.mjs +26 -29
- package/test/create-chores.test.mjs +12 -0
- package/test/fake-bd.test.mjs +72 -0
- package/test/helpers/fake-bd.mjs +26 -6
- package/test/helpers/fake-pi.mjs +132 -0
- package/test/pi-installer.test.mjs +345 -286
- package/test/pi-register.test.mjs +342 -0
- package/test/pi-sweep.test.mjs +619 -0
- package/test/release-gates.test.mjs +190 -0
- package/test/verify-worker-tree.test.mjs +120 -0
- package/token-map.README.md +7 -6
- package/token-map.json +6 -5
- package/extensions/agent-guardrails/node_modules/.bin/pi +0 -21
- package/extensions/agent-guardrails/node_modules/.bin/tsc +0 -21
- package/extensions/agent-guardrails/node_modules/.bin/tsserver +0 -21
- package/extensions/agent-guardrails/node_modules/.bin/tsx +0 -21
- package/extensions/agent-modes/node_modules/.bin/pi +0 -21
- package/extensions/agent-modes/node_modules/.bin/tsc +0 -21
- package/extensions/agent-modes/node_modules/.bin/tsserver +0 -21
- package/extensions/agent-modes/node_modules/.bin/tsx +0 -21
- package/extensions/session-start.ts +0 -44
- package/extensions/skill-stats/node_modules/.bin/pi +0 -21
- package/extensions/skill-stats/node_modules/.bin/tsc +0 -21
- package/extensions/skill-stats/node_modules/.bin/tsserver +0 -21
- package/extensions/skill-stats/node_modules/.bin/tsx +0 -21
- package/extensions/subagent/node_modules/.bin/pi +0 -21
- package/extensions/subagent/node_modules/.bin/pi-ai +0 -21
- package/extensions/subagent/node_modules/.bin/tsc +0 -21
- package/extensions/subagent/node_modules/.bin/tsserver +0 -21
- package/extensions/subagent/node_modules/.bin/tsx +0 -21
- package/lib/pi-packages.mjs +0 -169
- package/skills/planning/create-task/scripts/node_modules/.bin/tsc +0 -21
- package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +0 -21
- package/skills/planning/create-task/scripts/node_modules/.bin/tsx +0 -21
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +0 -21
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +0 -21
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +0 -21
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +0 -21
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +0 -21
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +0 -21
- package/test/pi-packages.test.mjs +0 -295
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
|
|
9
10
|
export type AgentScope = "user" | "project" | "both";
|
|
@@ -23,6 +24,25 @@ export interface AgentDiscoveryResult {
|
|
|
23
24
|
projectAgentsDir: string | null;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Parse a frontmatter `tools` value into PI tool names.
|
|
29
|
+
*
|
|
30
|
+
* `parseFrontmatter` returns the scalar as a single string, and agent files are
|
|
31
|
+
* written with either a comma-separated list (`tools: read, grep`) or a
|
|
32
|
+
* whitespace-separated one. Splitting on commas alone collapses the whitespace
|
|
33
|
+
* form into one bogus name, which becomes a `--tools` allowlist matching no
|
|
34
|
+
* tool — the subagent then runs with an empty tool block. Accept both
|
|
35
|
+
* separators so neither form can zero out the tool list.
|
|
36
|
+
*
|
|
37
|
+
* @param {string | undefined} tools Raw frontmatter `tools` scalar
|
|
38
|
+
* @returns {string[] | undefined} Tool names, or undefined when there are none
|
|
39
|
+
*/
|
|
40
|
+
export function parseToolList(tools: string | undefined): string[] | undefined {
|
|
41
|
+
const parsed = tools?.split(/[,\s]+/).filter(Boolean);
|
|
42
|
+
|
|
43
|
+
return parsed && parsed.length > 0 ? parsed : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
27
47
|
const agents: AgentConfig[] = [];
|
|
28
48
|
|
|
@@ -49,22 +69,17 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
49
69
|
continue;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
|
-
const { frontmatter, body } = parseFrontmatter<Record<string,
|
|
72
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
|
|
53
73
|
|
|
54
|
-
if (
|
|
74
|
+
if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") {
|
|
55
75
|
continue;
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
const tools = frontmatter.tools
|
|
59
|
-
?.split(",")
|
|
60
|
-
.map((t: string) => t.trim())
|
|
61
|
-
.filter(Boolean);
|
|
62
|
-
|
|
63
78
|
agents.push({
|
|
64
79
|
name: frontmatter.name,
|
|
65
80
|
description: frontmatter.description,
|
|
66
|
-
tools:
|
|
67
|
-
model: frontmatter.model,
|
|
81
|
+
tools: readTools(frontmatter.tools),
|
|
82
|
+
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
68
83
|
systemPrompt: body,
|
|
69
84
|
source,
|
|
70
85
|
filePath,
|
|
@@ -74,6 +89,21 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
74
89
|
return agents;
|
|
75
90
|
}
|
|
76
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The tools of a frontmatter `tools` value, which is written either as a YAML
|
|
94
|
+
* scalar (one string) or as a YAML sequence (an array).
|
|
95
|
+
*/
|
|
96
|
+
function readTools(value: unknown): string[] | undefined {
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
const tools = value
|
|
99
|
+
.filter((tool): tool is string => typeof tool === "string")
|
|
100
|
+
.map((tool) => tool.trim())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
return tools.length > 0 ? tools : undefined;
|
|
103
|
+
}
|
|
104
|
+
return parseToolList(typeof value === "string" ? value : undefined);
|
|
105
|
+
}
|
|
106
|
+
|
|
77
107
|
function isDirectory(p: string): boolean {
|
|
78
108
|
try {
|
|
79
109
|
return fs.statSync(p).isDirectory();
|
|
@@ -94,11 +124,72 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
|
94
124
|
}
|
|
95
125
|
}
|
|
96
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The package this extension ships inside: `<extension>/../..`, i.e. the repo
|
|
129
|
+
* checkout or the staged pi package.
|
|
130
|
+
*/
|
|
131
|
+
const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* True when `agentsDir` is a rendered tree — flat `*.agent.md` files and no
|
|
135
|
+
* composable `<name>/agent.md` source directories.
|
|
136
|
+
*/
|
|
137
|
+
function isRenderedAgentsTree(agentsDir: string): boolean {
|
|
138
|
+
if (!isDirectory(agentsDir)) return false;
|
|
139
|
+
let entries: fs.Dirent[];
|
|
140
|
+
try {
|
|
141
|
+
entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return !entries.some((entry) => entry.isDirectory() && fs.existsSync(path.join(agentsDir, entry.name, "agent.md")));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Directories the "user" scope discovers agents from, in precedence order.
|
|
150
|
+
*
|
|
151
|
+
* pi's own user agent dir comes first: it holds hand-written personal agents
|
|
152
|
+
* and the copies older installers made, so existing setups keep resolving what
|
|
153
|
+
* they did before. Second is the package this extension ships inside, which is
|
|
154
|
+
* what makes a staged pi package self-contained — `agent-cortex install pi`
|
|
155
|
+
* renders the flat pi agents into `<package>/agents`.
|
|
156
|
+
*
|
|
157
|
+
* A source checkout contributes nothing: its `agents/` tree is the composable
|
|
158
|
+
* source (composed at runtime by the agent-modes extension) alongside flat
|
|
159
|
+
* renders for other harnesses — Copilot's, whose tool names pi does not have.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveUserAgentDirs(packageRoot: string = PACKAGE_ROOT): string[] {
|
|
162
|
+
const dirs: string[] = [];
|
|
163
|
+
const add = (dir: string) => {
|
|
164
|
+
const resolved = path.resolve(dir);
|
|
165
|
+
if (!dirs.includes(resolved) && isDirectory(resolved)) dirs.push(resolved);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
add(path.join(getAgentDir(), "agents"));
|
|
169
|
+
const packageAgentsDir = path.join(packageRoot, "agents");
|
|
170
|
+
if (isRenderedAgentsTree(packageAgentsDir)) add(packageAgentsDir);
|
|
171
|
+
|
|
172
|
+
return dirs;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Load the user-scope agents from `dirs`, first directory wins on a name
|
|
177
|
+
* collision. Missing directories contribute nothing.
|
|
178
|
+
*/
|
|
179
|
+
export function loadUserAgents(dirs: string[]): AgentConfig[] {
|
|
180
|
+
const byName = new Map<string, AgentConfig>();
|
|
181
|
+
for (const dir of dirs) {
|
|
182
|
+
for (const agent of loadAgentsFromDir(dir, "user")) {
|
|
183
|
+
if (!byName.has(agent.name)) byName.set(agent.name, agent);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return Array.from(byName.values());
|
|
187
|
+
}
|
|
188
|
+
|
|
97
189
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
98
|
-
const userDir = path.join(getAgentDir(), "agents");
|
|
99
190
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
100
191
|
|
|
101
|
-
const userAgents = scope === "project" ? [] :
|
|
192
|
+
const userAgents = scope === "project" ? [] : loadUserAgents(resolveUserAgentDirs());
|
|
102
193
|
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
103
194
|
|
|
104
195
|
const agentMap = new Map<string, AgentConfig>();
|
|
@@ -461,7 +461,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
461
461
|
description: [
|
|
462
462
|
"Delegate tasks to specialized subagents with isolated context.",
|
|
463
463
|
"Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
|
|
464
|
-
'Default agent scope is "user"
|
|
464
|
+
'Default agent scope is "user": ~/.pi/agent/agents plus the agents/ tree of the package',
|
|
465
|
+
"this extension ships inside.",
|
|
465
466
|
'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").',
|
|
466
467
|
].join(" "),
|
|
467
468
|
parameters: SubagentParams,
|
package/lib/cli.mjs
CHANGED
|
@@ -9,7 +9,6 @@ const INSTALL_OPTIONS = {
|
|
|
9
9
|
"--output": { key: "output" },
|
|
10
10
|
"--plugin-root": { key: "pluginRoot" },
|
|
11
11
|
"--require-register": { key: "requireRegister", boolean: true },
|
|
12
|
-
"--no-provision": { key: "noProvision", boolean: true },
|
|
13
12
|
};
|
|
14
13
|
|
|
15
14
|
export function parseArgs(argv) {
|
|
@@ -94,16 +93,43 @@ Commands:
|
|
|
94
93
|
plugin.json "agents" scans)
|
|
95
94
|
--dry-run Print what would be generated without writing
|
|
96
95
|
Options (pi):
|
|
97
|
-
--
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
(no --output) Materialise the pi package into
|
|
97
|
+
~/.agent-cortex/pi: package.json,
|
|
98
|
+
agents/, skills/, extensions/ and
|
|
99
|
+
the bundled third-party pi packages
|
|
100
|
+
(pi-questions, pi-web-access), then
|
|
101
|
+
REGISTER that directory with pi via
|
|
102
|
+
"pi install" (user scope, idempotent
|
|
103
|
+
by state: a re-run is a no-op update,
|
|
104
|
+
and stale raw-repo sources are
|
|
105
|
+
removed so one source remains).
|
|
106
|
+
Once registered, the artifacts the
|
|
107
|
+
pre-staged-model installer rendered
|
|
108
|
+
into ~/.pi/agent are removed in a
|
|
109
|
+
one-time migration: agents and
|
|
110
|
+
skills whose names this package
|
|
111
|
+
ships or once shipped. Anything
|
|
112
|
+
else there is left alone, including
|
|
113
|
+
the whole extensions/ directory.
|
|
114
|
+
A migration that removed every
|
|
115
|
+
copy it attributed is recorded in
|
|
116
|
+
~/.agent-cortex/legacy-sweep.json
|
|
117
|
+
(keyed by the pi user directory)
|
|
118
|
+
and never runs there twice; one
|
|
119
|
+
that could not be completed warns
|
|
120
|
+
and is retried on the next install.
|
|
121
|
+
--output <dir> Stage the package into <dir> only —
|
|
122
|
+
no registration (default:
|
|
123
|
+
~/.agent-cortex/pi)
|
|
124
|
+
--dry-run Show what would be staged and
|
|
125
|
+
registered without writing or spawning
|
|
101
126
|
--plugin-root <dir> Override the plugin root used for {{PATH:...}}
|
|
102
127
|
tokens (default: token-map.json's pi value)
|
|
103
|
-
--
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
--require-register
|
|
129
|
+
Fail the install when the pi CLI
|
|
130
|
+
cannot register the staged package
|
|
131
|
+
(missing from PATH); default warns and
|
|
132
|
+
prints the manual commands
|
|
107
133
|
--help, -h Show this help text`;
|
|
108
134
|
}
|
|
109
135
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaybeeuu/agent-cortex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.43.0",
|
|
4
4
|
"description": "Personal PI package with custom agents, skills, and extensions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
7
7
|
],
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"repository":
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/jaybeeuu/agent-cortex"
|
|
12
|
+
},
|
|
10
13
|
"bin": {
|
|
11
14
|
"agent-cortex": "./bin/agent-cortex.mjs"
|
|
12
15
|
},
|
|
@@ -24,12 +27,12 @@
|
|
|
24
27
|
],
|
|
25
28
|
"skills": [
|
|
26
29
|
"./skills"
|
|
27
|
-
],
|
|
28
|
-
"packages": [
|
|
29
|
-
"npm:pi-questions",
|
|
30
|
-
"npm:pi-web-access@0.10.7"
|
|
31
30
|
]
|
|
32
31
|
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"pi-questions": "^0.3.4",
|
|
34
|
+
"pi-web-access": "0.10.7"
|
|
35
|
+
},
|
|
33
36
|
"scripts": {
|
|
34
37
|
"build": "echo 'Build complete — no compilation needed for PI package'",
|
|
35
38
|
"lint": "pnpm typecheck && pnpm check:sync",
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
"changeset": "changeset",
|
|
41
44
|
"version-packages": "changeset version && bash scripts/sync-plugin-version.sh && pnpm build:copilot",
|
|
42
45
|
"publish-package": "TARBALL=$(pnpm pack | tail -n 1) && npm publish \"$TARBALL\" --provenance --access public",
|
|
46
|
+
"publish-noop": "echo 'No publish: the merged PR carries the no-publish label.'",
|
|
43
47
|
"release": "changeset publish",
|
|
44
48
|
"postinstall": "bash scripts/install-instructions.sh"
|
|
45
49
|
}
|
package/pi/settings.json
CHANGED
|
@@ -6,15 +6,9 @@
|
|
|
6
6
|
},
|
|
7
7
|
"lastChangelogVersion": "0.87.1",
|
|
8
8
|
"packages": [
|
|
9
|
-
{
|
|
10
|
-
"source": "../../src/agent-cortex",
|
|
11
|
-
"skills": []
|
|
12
|
-
},
|
|
13
|
-
"npm:pi-web-access@0.10.7",
|
|
14
9
|
"npm:@getpipher/vision",
|
|
15
10
|
"npm:@hypabolic/pi-hypa",
|
|
16
|
-
"npm:context-mode"
|
|
17
|
-
"npm:pi-questions"
|
|
11
|
+
"npm:context-mode"
|
|
18
12
|
],
|
|
19
13
|
"defaultProvider": "opencode-go",
|
|
20
14
|
"defaultModel": "deepseek-v4.1-flash",
|
package/plugin.json
CHANGED
package/pnpm-workspace.yaml
CHANGED
|
@@ -5,4 +5,10 @@ packages:
|
|
|
5
5
|
allowBuilds:
|
|
6
6
|
'@google/genai': false
|
|
7
7
|
esbuild: false
|
|
8
|
+
# koffi (an optionalDependency of @mariozechner/pi-tui, which pi-questions
|
|
9
|
+
# peers on) ships prebuilt build/koffi/<platform>_<arch>/*.node binaries that
|
|
10
|
+
# index.js loads directly. Its install script (cnoke --prebuild) only
|
|
11
|
+
# matters for toolchains with no shipped binary, so deny it: acknowledged
|
|
12
|
+
# explicitly to satisfy the strict allowlist, no compilation, no download.
|
|
13
|
+
koffi: false
|
|
8
14
|
protobufjs: false
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// PR gate: a pull request must add a changeset (.changeset/*.md) unless it is
|
|
2
|
+
// the changesets bot's Version Packages PR or carries the `no-publish` label.
|
|
3
|
+
// Reads `git diff --name-status <base>...HEAD` on stdin and the PR metadata
|
|
4
|
+
// from the environment (CI supplies all three).
|
|
5
|
+
import { evaluateChangesetGate, parseNameStatus } from "./lib/release-gates.mjs";
|
|
6
|
+
|
|
7
|
+
async function readStdin() {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
10
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readLabels(raw) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
} catch {
|
|
17
|
+
console.error("::error::PR_LABELS must be a JSON array of label names.");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const labels = readLabels(process.env.PR_LABELS ?? "[]");
|
|
23
|
+
const changes = parseNameStatus(await readStdin());
|
|
24
|
+
const result = evaluateChangesetGate({
|
|
25
|
+
changes,
|
|
26
|
+
labels,
|
|
27
|
+
headRef: process.env.PR_HEAD_REF ?? "",
|
|
28
|
+
actor: process.env.PR_ACTOR ?? "",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (result.ok) {
|
|
32
|
+
console.log(`✓ Changeset gate passed (${result.reason}).`);
|
|
33
|
+
} else {
|
|
34
|
+
console.error(
|
|
35
|
+
[
|
|
36
|
+
"::error::This PR adds no changeset (.changeset/*.md).",
|
|
37
|
+
"Publishing is the default: write a changeset file (see the style-versioning skill)",
|
|
38
|
+
"and commit it, or label the PR 'no-publish' when it genuinely ships nothing.",
|
|
39
|
+
].join("\n"),
|
|
40
|
+
);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Decision logic for the two release gates that stop `main` re-publishing an
|
|
2
|
+
// already-published version. Both gates are pure: the CLI adapters read git
|
|
3
|
+
// diffs, env vars, and the GitHub API, then delegate the decision here.
|
|
4
|
+
//
|
|
5
|
+
// Model: publishing is the default. The only path to "no publish" is an
|
|
6
|
+
// explicit `no-publish` label on the merged PR — skipping is never implicit.
|
|
7
|
+
|
|
8
|
+
export const NO_PUBLISH_LABEL = "no-publish";
|
|
9
|
+
export const PUBLISH_COMMAND = "pnpm publish-package";
|
|
10
|
+
export const NO_PUBLISH_COMMAND = "pnpm publish-noop";
|
|
11
|
+
|
|
12
|
+
// The changesets bot's Version Packages PR consumes changesets and adds none.
|
|
13
|
+
const VERSION_PACKAGES_HEAD_REF = "changeset-release/";
|
|
14
|
+
const GITHUB_ACTIONS_BOT = "github-actions[bot]";
|
|
15
|
+
|
|
16
|
+
const CHANGESET_PATH = /^\.changeset\/[^/]+\.md$/;
|
|
17
|
+
const CHANGESET_EXEMPT_PATHS = new Set([".changeset/README.md"]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse `git diff --name-status` output into `{ status, path }` entries, where
|
|
21
|
+
* `status` is the single-letter change type and `path` is the new path.
|
|
22
|
+
*/
|
|
23
|
+
export function parseNameStatus(output) {
|
|
24
|
+
return output
|
|
25
|
+
.split("\n")
|
|
26
|
+
.filter((line) => line.trim() !== "")
|
|
27
|
+
.map((line) => {
|
|
28
|
+
const [status, ...paths] = line.split("\t");
|
|
29
|
+
return { status: status.charAt(0), path: paths.at(-1) };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decide whether a pull request may skip adding a changeset. Exempt only the
|
|
35
|
+
* Version Packages PR and PRs explicitly labelled `no-publish`; every other PR
|
|
36
|
+
* must add its own changeset.
|
|
37
|
+
*/
|
|
38
|
+
export function evaluateChangesetGate({ changes, labels, headRef, actor }) {
|
|
39
|
+
if (headRef.startsWith(VERSION_PACKAGES_HEAD_REF) || actor === GITHUB_ACTIONS_BOT) {
|
|
40
|
+
return { ok: true, reason: "version-packages-pr" };
|
|
41
|
+
}
|
|
42
|
+
if (labels.includes(NO_PUBLISH_LABEL)) {
|
|
43
|
+
return { ok: true, reason: "no-publish-label" };
|
|
44
|
+
}
|
|
45
|
+
if (changes.some((change) => change.status === "A" && isChangeset(change.path))) {
|
|
46
|
+
return { ok: true, reason: "changeset-added" };
|
|
47
|
+
}
|
|
48
|
+
return { ok: false, reason: "missing-changeset" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the command changesets/action should exec for publishing, based on
|
|
53
|
+
* the labels of the PR merged for the pushed commit.
|
|
54
|
+
*/
|
|
55
|
+
export function resolvePublishCommand(labels) {
|
|
56
|
+
return labels.includes(NO_PUBLISH_LABEL) ? NO_PUBLISH_COMMAND : PUBLISH_COMMAND;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isChangeset(path) {
|
|
60
|
+
return CHANGESET_PATH.test(path) && !CHANGESET_EXEMPT_PATHS.has(path);
|
|
61
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Verify that a dispatched pipeline-stage worker actually delivered.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: a worker's `---REPORT---` is a claim, not evidence. A stage
|
|
4
|
+
// that reports `OUTCOME: SUCCESS` and leaves its work uncommitted looks
|
|
5
|
+
// identical, from the report alone, to one that committed and pushed — and the
|
|
6
|
+
// orchestrator opens the feature PR from the branch, so uncommitted work is
|
|
7
|
+
// silently dropped. The observed failure was exactly that: workers reporting
|
|
8
|
+
// success with a dirty worktree, every one of them needing a human to finish
|
|
9
|
+
// the tree by hand.
|
|
10
|
+
//
|
|
11
|
+
// This module answers one question against the worktree itself: did this worker
|
|
12
|
+
// leave a clean tree with at least one commit ahead of where it started? It
|
|
13
|
+
// reads nothing the worker wrote, so it cannot be fooled by the worker's prose.
|
|
14
|
+
//
|
|
15
|
+
// Usage (CLI):
|
|
16
|
+
// node scripts/lib/verify-worker-tree.mjs --worktree <dir> --base <sha>
|
|
17
|
+
// node scripts/lib/verify-worker-tree.mjs --worktree <dir> --base <ref> --json
|
|
18
|
+
//
|
|
19
|
+
// Exit code is 0 only when the worker delivered, so a caller can gate on it.
|
|
20
|
+
|
|
21
|
+
import { execFile } from "node:child_process";
|
|
22
|
+
import { promisify } from "node:util";
|
|
23
|
+
import { resolve } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
27
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
28
|
+
|
|
29
|
+
async function git(worktree, args) {
|
|
30
|
+
const { stdout } = await execFileAsync("git", args, { cwd: worktree });
|
|
31
|
+
return stdout.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function countPaths(porcelain) {
|
|
35
|
+
return porcelain.split("\n").filter((line) => line.trim() !== "").length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check that a worker delivered: clean worktree and at least one commit ahead
|
|
40
|
+
* of `base`. Returns the reasons it failed, so the caller can surface them
|
|
41
|
+
* instead of guessing why a stage did not advance.
|
|
42
|
+
*/
|
|
43
|
+
export async function verifyWorkerTree({ worktree, base }) {
|
|
44
|
+
const reasons = [];
|
|
45
|
+
|
|
46
|
+
const dirty = await git(worktree, ["status", "--porcelain"]);
|
|
47
|
+
if (dirty !== "") {
|
|
48
|
+
reasons.push(`worktree is dirty: ${countPaths(dirty)} uncommitted path(s)`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ahead = Number(await git(worktree, ["rev-list", "--count", `${base}..HEAD`]));
|
|
52
|
+
if (!Number.isFinite(ahead) || ahead < 1) {
|
|
53
|
+
reasons.push(`no commit ahead of base ${base.slice(0, 7)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { ok: reasons.length === 0, reasons, commitsAhead: ahead };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function parseFlags(argv) {
|
|
62
|
+
const flags = {};
|
|
63
|
+
for (let i = 0; i < argv.length; i++) {
|
|
64
|
+
const arg = argv[i];
|
|
65
|
+
if (!arg.startsWith("--")) continue;
|
|
66
|
+
const key = arg.slice(2);
|
|
67
|
+
const next = argv[i + 1];
|
|
68
|
+
if (next === undefined || next.startsWith("--")) {
|
|
69
|
+
flags[key] = true;
|
|
70
|
+
} else {
|
|
71
|
+
flags[key] = next;
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return flags;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function required(flags, name) {
|
|
79
|
+
const value = flags[name];
|
|
80
|
+
if (typeof value !== "string" || value === "") {
|
|
81
|
+
console.error(`verify-worker-tree: --${name} <value> is required`);
|
|
82
|
+
process.exit(2);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main(argv) {
|
|
88
|
+
const flags = parseFlags(argv);
|
|
89
|
+
const verdict = await verifyWorkerTree({
|
|
90
|
+
worktree: required(flags, "worktree"),
|
|
91
|
+
base: required(flags, "base"),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (flags.json) {
|
|
95
|
+
console.log(JSON.stringify(verdict, null, 2));
|
|
96
|
+
} else if (verdict.ok) {
|
|
97
|
+
console.log(`ok: ${verdict.commitsAhead} commit(s) ahead of base, tree clean`);
|
|
98
|
+
} else {
|
|
99
|
+
for (const reason of verdict.reasons) console.error(`failed: ${reason}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
process.exit(verdict.ok ? 0 : 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (process.argv[1] && resolve(process.argv[1]) === SELF) {
|
|
106
|
+
await main(process.argv.slice(2));
|
|
107
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Release gate: print the single-token publish command changesets/action
|
|
2
|
+
// should exec. The merged PR's labels arrive as a JSON array in argv[2]; the
|
|
3
|
+
// action splits the printed command on whitespace with no shell, so it must
|
|
4
|
+
// stay a plain `pnpm <script>` token pair.
|
|
5
|
+
import { resolvePublishCommand } from "./lib/release-gates.mjs";
|
|
6
|
+
|
|
7
|
+
function readLabels(raw) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
} catch {
|
|
11
|
+
console.error("::error::labels argument must be a JSON array of label names.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const labels = readLabels(process.argv[2] ?? "[]");
|
|
17
|
+
process.stdout.write(`${resolvePublishCommand(labels)}\n`);
|
|
File without changes
|
|
@@ -27,7 +27,7 @@ description: "Grilling session that challenges your plan against the existing do
|
|
|
27
27
|
5. **Cross-reference with code.** When the user states how something works, check whether the code agrees. Surface contradictions the moment you find them.
|
|
28
28
|
6. **Update `CONTEXT.md` inline.** When a term resolves, write the entry right there — do not batch. Keep it a pure glossary: no implementation details, no specs, no scratch notes. Entry format and file-discovery rules live in `REFERENCE.md`.
|
|
29
29
|
7. **Offer ADRs sparingly.** Propose an ADR only when all three hold: hard to reverse, surprising without context, and the result of a real trade-off. If any is missing, skip it. ADR format lives in `REFERENCE.md`.
|
|
30
|
-
8. **Close when shared understanding is reached.** Confirm every design-tree branch is resolved or explicitly deferred,
|
|
30
|
+
8. **Close when shared understanding is reached.** Confirm every design-tree branch is resolved or explicitly deferred, that the glossary diff captures the session's new language, and — when the session feeds a bead — that there is enough agreed detail to fill every required section of the target bead type (see `bd-tool` → Required sections). Never invent section content; surface assumptions and confirm them.
|
|
31
31
|
|
|
32
32
|
## Red Flags
|
|
33
33
|
|
|
@@ -88,4 +88,5 @@ which voids an order before fulfilment. See cancellation.
|
|
|
88
88
|
- [ ] Terminology conflicts and vague terms called out the moment they appeared
|
|
89
89
|
- [ ] Claims cross-referenced against code; contradictions surfaced
|
|
90
90
|
- [ ] ADRs offered only when all three criteria held (offering none is a valid outcome)
|
|
91
|
-
- [ ] All design-tree branches resolved or explicitly deferred before closing
|
|
91
|
+
- [ ] All design-tree branches resolved or explicitly deferred before closing
|
|
92
|
+
- [ ] When feeding a bead, enough agreed detail to fill every required section; no fabricated answers
|
|
@@ -43,6 +43,11 @@ Describe the architectural friction:
|
|
|
43
43
|
- What integration risk exists in the seams between them
|
|
44
44
|
- Why this makes the codebase harder to navigate and maintain
|
|
45
45
|
|
|
46
|
+
## Acceptance Criteria
|
|
47
|
+
|
|
48
|
+
- [ ] <verifiable outcome 1>
|
|
49
|
+
- [ ] <verifiable outcome 2>
|
|
50
|
+
|
|
46
51
|
## Proposed Interface
|
|
47
52
|
|
|
48
53
|
The chosen interface design:
|
|
@@ -43,7 +43,7 @@ A **deep module** (John Ousterhout, "A Philosophy of Software Design") has a sma
|
|
|
43
43
|
|
|
44
44
|
6. **User picks an interface** (or accepts the recommendation).
|
|
45
45
|
|
|
46
|
-
7. **Create the refactor bead.** Run the `create-task` skill with a concise title, the description from the template in [REFERENCE.md](REFERENCE.md), and priority P2. Always use `create-task` — never `bd create` directly — because it classifies the bead and expands AFK work into pipeline stages ralph can execute. Do not ask the user to review before creating; share the bead ID.
|
|
46
|
+
7. **Create the refactor bead.** Run the `create-task` skill with a concise title, the description from the template in [REFERENCE.md](REFERENCE.md), and priority P2. The template's `## Acceptance Criteria` is required (enforced by `create-task`'s `--validate`) and must reflect outcomes the user agreed, not assumptions. Always use `create-task` — never `bd create` directly — because it classifies the bead and expands AFK work into pipeline stages ralph can execute. Do not ask the user to review before creating; share the bead ID.
|
|
47
47
|
|
|
48
48
|
## Red Flags
|
|
49
49
|
|
|
@@ -95,5 +95,6 @@ Output: numbered deepening candidates (cluster, coupling, dependency category, t
|
|
|
95
95
|
- [ ] At least two radically different designs, each with signature, usage example, hidden complexity, dependency strategy, and trade-offs
|
|
96
96
|
- [ ] Recommendation given in prose, with a hybrid proposed where elements combine well
|
|
97
97
|
- [ ] Refactor bead created via `create-task` at P2 using the REFERENCE.md description template
|
|
98
|
+
- [ ] Description carries agreed `## Acceptance Criteria`; the bead lints clean
|
|
98
99
|
- [ ] Bead ID reported and no review gate imposed before creation
|
|
99
100
|
- [ ] No unrelated areas explored or refactors proposed outside the chosen candidate
|
|
@@ -5,7 +5,7 @@ description: Create a task bead, classify it as AFK or HITL, and if AFK expand i
|
|
|
5
5
|
|
|
6
6
|
# Create Task
|
|
7
7
|
|
|
8
|
-
Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or human-in-the-loop (HITL) execution, and — when AFK — expand it into the standard pipeline: one chore bead per stage plus a HITL PR gate task so a human always reviews the finished branch. Parameters: `title` (required), `description` (required), `priority` (0–3, default 2), `parent` (epic bead ID, optional — sets a `parent-child` dependency).
|
|
8
|
+
Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or human-in-the-loop (HITL) execution, and — when AFK — expand it into the standard pipeline: one chore bead per stage plus a HITL PR gate task so a human always reviews the finished branch. Parameters: `title` (required), `description` (required), `priority` (0–3, default 2), `parent` (epic bead ID, optional — sets a `parent-child` dependency). The description must carry the task's required section — `## Acceptance Criteria` — which `--validate` enforces; fill it from agreed answers, not assumptions (see `bd-tool` → Required sections).
|
|
9
9
|
|
|
10
10
|
## When to use
|
|
11
11
|
|
|
@@ -21,10 +21,13 @@ Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or hu
|
|
|
21
21
|
|
|
22
22
|
## Workflow
|
|
23
23
|
|
|
24
|
-
1. **Create the parent bead.**
|
|
24
|
+
1. **Create the parent bead.** The description must contain the task's required section —
|
|
25
|
+
`## Acceptance Criteria` — or `--validate` rejects creation. Fill it from agreed answers, not
|
|
26
|
+
assumptions (see `bd-tool` → Required sections).
|
|
25
27
|
|
|
26
28
|
```bash
|
|
27
|
-
bd create "<title>" --description "<description>" --priority <priority>
|
|
29
|
+
bd create "<title>" --description "<description>" --priority <priority> --validate
|
|
30
|
+
bd lint <new-id>
|
|
28
31
|
```
|
|
29
32
|
|
|
30
33
|
If a `parent` epic was provided, record the dependency:
|
|
@@ -65,6 +68,7 @@ Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or hu
|
|
|
65
68
|
|
|
66
69
|
## Red Flags
|
|
67
70
|
|
|
71
|
+
- **Filling acceptance criteria with assumptions.** Fabricated criteria pass `--validate` but mislead the pipeline. The description is the interview's output — confirm it with the human instead.
|
|
68
72
|
- **Running the script from the wrong directory.** Chores land in whatever `.beads` tree `bd` resolves from cwd — the target project's root is the only safe cwd.
|
|
69
73
|
- **Spawning the `classify-bead` subagent when the classifier already resolved the bead.** The deterministic script is the short-circuit; a subagent is only for `escalate: true`.
|
|
70
74
|
- **Skipping the classifier and classifying by hand.** The rubric is a subagent's job; a hand-rolled guess drifts from it.
|
|
@@ -101,6 +105,7 @@ The full bead-property contract (titles, labels, dependencies per chore and for
|
|
|
101
105
|
## Verification checklist
|
|
102
106
|
|
|
103
107
|
- [ ] Parent bead exists with the correct title, description, and priority.
|
|
108
|
+
- [ ] Parent bead created with `--validate`, and `bd lint <id>` reports no missing sections.
|
|
104
109
|
- [ ] `parent-child` dependency added when a parent epic was supplied.
|
|
105
110
|
- [ ] `implementation-type` label present on the parent bead (`bd label list <id>`) — resolved by the classifier or, on escalation, the rubric subagent.
|
|
106
111
|
- [ ] HITL path: no pipeline chores created, caller told the classification. AFK path: script ran from the target project's cwd and its JSON contains every stage ID plus `featurePrReview`.
|
|
@@ -101,8 +101,11 @@ const prGateBeadId = run([
|
|
|
101
101
|
'bd', 'create', JSON.stringify(`[${parentId}] PR Review and Merge`),
|
|
102
102
|
'--type', 'task',
|
|
103
103
|
'--description', JSON.stringify(
|
|
104
|
-
'Human review gate: review and merge the agent PR into the feature branch before closing this feature.',
|
|
104
|
+
'Human review gate: review and merge the agent PR into the feature branch before closing this feature.\n\n## Acceptance Criteria\n\n- [ ] Agent PR reviewed and merged into the feature branch.',
|
|
105
105
|
),
|
|
106
|
+
// The gate is type `task`, so `--validate` enforces its `## Acceptance Criteria`
|
|
107
|
+
// section. Stage chores require no sections and deliberately omit the flag.
|
|
108
|
+
'--validate',
|
|
106
109
|
'--priority', priority,
|
|
107
110
|
'--labels', 'implementation-type:hitl,lifecycle:feature-pr',
|
|
108
111
|
'--parent', parentId,
|