@jaybeeuu/agent-cortex 2.1.0 → 2.3.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 +411 -0
- package/README.md +33 -13
- package/agents/README.md +22 -10
- package/agents/plan/agent.md +2 -2
- package/agents/ralph/agent.md +126 -88
- package/agents/ralph/claude/frontmatter.json +3 -3
- package/agents/ralph/claude/wake.md +46 -0
- package/agents/ralph/pi/{polling.md → wake.md} +35 -20
- package/agents/ralph-plan/agent.md +23 -11
- package/bin/agent-cortex.mjs +66 -4
- package/bin/installers/claude.mjs +182 -68
- package/bin/installers/pi.mjs +200 -24
- package/extensions/agent-guardrails/README.md +1 -1
- package/extensions/agent-guardrails/guardrails.ts +1 -1
- package/extensions/agent-guardrails/index.ts +1 -1
- package/extensions/agent-modes/discover.test.ts +2 -2
- package/extensions/output-style/README.md +1 -1
- package/extensions/output-style/index.ts +2 -2
- package/extensions/output-style/output-style.ts +2 -2
- package/hooks/claude/scripts/notify.mjs +8 -1
- package/lib/cli.mjs +91 -2
- package/package.json +27 -3
- package/scripts/check-changeset.mjs +4 -0
- package/scripts/check-no-sync.mjs +5 -1
- package/scripts/lib/compose-agent.mjs +69 -9
- package/scripts/lib/fs.mjs +10 -2
- package/scripts/lib/migrate.mjs +736 -0
- package/scripts/lib/release-gates.mjs +21 -1
- package/scripts/lib/skill-frontmatter.mjs +23 -6
- package/scripts/lib/skill-links.mjs +52 -0
- package/scripts/lib/verify-worker-tree.mjs +25 -0
- package/scripts/resolve-publish-command.mjs +4 -0
- package/skills/engineering/design-an-interface/SKILL.md +3 -3
- package/skills/engineering/grill-with-docs/SKILL.md +3 -5
- package/skills/engineering/improve-codebase-architecture/SKILL.md +5 -4
- package/skills/engineering/tdd/SKILL.md +5 -7
- package/skills/engineering/technical-direction/SKILL.md +5 -5
- package/skills/planning/classify-bead/REFERENCE.md +3 -3
- package/skills/planning/classify-bead/SKILL.md +14 -9
- package/skills/planning/classify-bead/scripts/classify-bead.mjs +5 -4
- package/skills/planning/create-task/REFERENCE.md +52 -3
- package/skills/planning/create-task/SKILL.md +70 -26
- package/skills/planning/create-task/scripts/{create-chores.ts → create-chores.mjs} +48 -23
- package/skills/planning/flesh-out-idea/REFERENCE.md +116 -0
- package/skills/planning/flesh-out-idea/SKILL.md +146 -0
- package/skills/planning/prd-to-tasks/REFERENCE.md +54 -5
- package/skills/planning/prd-to-tasks/SKILL.md +22 -16
- package/skills/planning/record-idea/FORMAT.md +66 -17
- package/skills/planning/record-idea/SKILL.md +48 -24
- package/skills/planning/request-refactor-plan/SKILL.md +4 -4
- package/skills/planning/spec-to-tickets/SKILL.md +5 -5
- package/skills/planning/write-a-prd/SKILL.md +8 -9
- package/skills/planning/write-a-ticket/SKILL.md +4 -6
- package/skills/productivity/bd-tool/SKILL.md +15 -10
- package/skills/productivity/grill-me/SKILL.md +5 -0
- package/skills/productivity/init-beads/SKILL.md +3 -3
- package/skills/productivity/migration/REFERENCE.md +227 -0
- package/skills/productivity/migration/SKILL.md +188 -0
- package/skills/productivity/validate-agent-dir/scripts/{validate-agent-dir.ts → validate-agent-dir.mjs} +49 -28
- package/skills/productivity/write-a-skill/SKILL.md +5 -5
- package/skills/review/maintain-agent-docs/SKILL.md +5 -7
- package/skills/review/refactor-skill/SKILL.md +4 -3
- package/skills/review/review-security/SKILL.md +3 -5
- package/skills/style/style-code/SKILL.md +5 -10
- package/skills/style/style-comms/SKILL.md +5 -7
- package/skills/style/style-documentation/SKILL.md +5 -6
- package/skills/style/style-tests/SKILL.md +5 -7
- package/skills/workflow/git-workflow/SKILL.md +52 -26
- package/skills/workflow/hitl-collab/SKILL.md +4 -6
- package/skills/workflow/plan/SKILL.md +41 -30
- package/skills/workflow/ralph/REFERENCE.md +182 -51
- package/skills/workflow/ralph/SKILL.md +89 -50
- package/skills/workflow/run-pipeline-stage/SKILL.md +72 -71
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +11 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +12 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +12 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +11 -0
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +13 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +73 -1
- package/token-map.README.md +9 -4
- package/token-map.json +13 -6
- package/.agents/skills/style-versioning/SKILL.md +0 -172
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.github/workflows/ci.yml +0 -302
- package/.junie/guidelines.md +0 -87
- package/.junie/mcp/mcp.json +0 -10
- package/AGENTS.md +0 -153
- package/agents/ralph/claude/polling.md +0 -31
- package/agents-native/ralph.md +0 -203
- package/docs/claude-hooks.md +0 -144
- package/docs/commit-signing.md +0 -231
- package/docs/ideas/auto-model-selection.md +0 -75
- package/docs/ideas/ban-content-assertion-tests.md +0 -75
- package/docs/ideas/bead-progress-notes-for-session-resumption.md +0 -75
- package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +0 -70
- package/docs/ideas/ctx-alias-version-and-update-commands.md +0 -65
- package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +0 -67
- package/docs/ideas/dedicated-review-tests-skill.md +0 -71
- package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +0 -36
- package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +0 -99
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +0 -42
- package/docs/ideas/extension-manifests.md +0 -121
- package/docs/ideas/improve-pi-system-prompt.md +0 -43
- package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +0 -95
- package/docs/ideas/model-selector.md +0 -40
- package/docs/ideas/pi-task-memory.md +0 -34
- package/docs/ideas/qa-extension.md +0 -43
- package/docs/ideas/record-idea-implementation-questions.md +0 -68
- package/docs/ideas/record-ideas-as-beads.md +0 -115
- package/docs/ideas/self-flushing-session.md +0 -72
- package/docs/ideas/state-in-beads-not-md.md +0 -69
- package/docs/ideas/subagent-visibility.md +0 -72
- package/docs/ideas/task-session-context.local.md +0 -95
- package/docs/ideas/task-session-context.md +0 -89
- package/docs/ideas/test-data-creation-helpers.md +0 -92
- package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +0 -68
- package/docs/ideas/unified-ci-pipeline.md +0 -28
- package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +0 -71
- package/docs/ideas/worktree-housekeeping-for-closed-beads.md +0 -39
- package/docs/inspirations.md +0 -33
- package/docs/research/disable-model-invocation.md +0 -82
- package/docs/research/gas-city-research.md +0 -256
- package/docs/research/skill-design-research.md +0 -286
- package/docs/research/skill-improvements-analysis.md +0 -117
- package/docs/research/skill-patterns-research.md +0 -24
- package/docs/skills/skill-anatomy.md +0 -200
- package/docs/technical-direction/harness-config.md +0 -177
- package/docs/technical-direction/migration-execution-labels.md +0 -177
- package/docs/user-preferences.md +0 -17
- package/extensions/agent-guardrails/tsconfig.json +0 -12
- package/extensions/agent-modes/tsconfig.json +0 -12
- package/extensions/output-style/tsconfig.json +0 -12
- package/extensions/session-start/tsconfig.json +0 -12
- package/extensions/skill-stats/pnpm-lock.yaml +0 -329
- package/extensions/skill-stats/tsconfig.json +0 -12
- package/extensions/subagent/agents/planner.md +0 -37
- package/extensions/subagent/agents/reviewer.md +0 -35
- package/extensions/subagent/agents/scout.md +0 -50
- package/extensions/subagent/agents/worker.md +0 -24
- package/extensions/subagent/prompts/implement-and-review.md +0 -10
- package/extensions/subagent/prompts/implement.md +0 -10
- package/extensions/subagent/prompts/scout-and-plan.md +0 -9
- package/extensions/subagent/tsconfig.json +0 -12
- package/pi/keybindings.json +0 -3
- package/pi/settings.json +0 -34
- package/pnpm-workspace.yaml +0 -14
- package/skills/planning/create-task/scripts/package.json +0 -14
- package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
- package/skills/planning/create-task/scripts/tsconfig.json +0 -12
- package/skills/planning/record-idea/scripts/new-idea.sh +0 -89
- package/skills/productivity/validate-agent-dir/scripts/package.json +0 -15
- package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
- package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +0 -2
- package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +0 -19
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +0 -317
- package/skills/workflow/run-pipeline-stage/REFERENCE.md +0 -22
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +0 -682
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +0 -588
- package/skills/workflow/run-pipeline-stage/scripts/package.json +0 -15
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +0 -2
- package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +0 -12
- package/test/classify-bead.test.mjs +0 -277
- package/test/claude-installer.test.mjs +0 -308
- package/test/cli.test.mjs +0 -314
- package/test/create-chores.test.mjs +0 -85
- package/test/fake-bd.test.mjs +0 -72
- package/test/fs.test.mjs +0 -76
- package/test/helpers/fake-bd.mjs +0 -87
- package/test/helpers/fake-claude.mjs +0 -152
- package/test/helpers/fake-pi.mjs +0 -132
- package/test/installer.test.mjs +0 -806
- package/test/notify-hook.test.mjs +0 -63
- package/test/pending-gates.test.mjs +0 -147
- package/test/pi-installer.test.mjs +0 -785
- package/test/pi-register.test.mjs +0 -342
- package/test/pi-sweep.test.mjs +0 -619
- package/test/release-gates.test.mjs +0 -190
- package/test/skill-frontmatter.test.mjs +0 -137
- package/test/verify-worker-tree.test.mjs +0 -120
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
// Behaviour tests for the release gates that keep `main` from re-publishing a
|
|
2
|
-
// version: the inverted PR changeset gate and the no-publish release gate.
|
|
3
|
-
// The decision logic is pure; the two CLI adapters are exercised through their
|
|
4
|
-
// process boundary because that is the contract CI depends on.
|
|
5
|
-
|
|
6
|
-
import { describe, it } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import {
|
|
11
|
-
PUBLISH_COMMAND,
|
|
12
|
-
NO_PUBLISH_COMMAND,
|
|
13
|
-
evaluateChangesetGate,
|
|
14
|
-
parseNameStatus,
|
|
15
|
-
resolvePublishCommand,
|
|
16
|
-
} from "../scripts/lib/release-gates.mjs";
|
|
17
|
-
|
|
18
|
-
const CHECK_CHANGESET = fileURLToPath(new URL("../scripts/check-changeset.mjs", import.meta.url));
|
|
19
|
-
const RESOLVE_PUBLISH = fileURLToPath(
|
|
20
|
-
new URL("../scripts/resolve-publish-command.mjs", import.meta.url),
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
function runScript(script, { args = [], env = {}, stdin = "" } = {}) {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
const child = spawn(process.execPath, [script, ...args], {
|
|
26
|
-
env: { ...process.env, ...env },
|
|
27
|
-
});
|
|
28
|
-
let stdout = "";
|
|
29
|
-
let stderr = "";
|
|
30
|
-
child.stdout.on("data", (chunk) => {
|
|
31
|
-
stdout += chunk;
|
|
32
|
-
});
|
|
33
|
-
child.stderr.on("data", (chunk) => {
|
|
34
|
-
stderr += chunk;
|
|
35
|
-
});
|
|
36
|
-
child.on("error", reject);
|
|
37
|
-
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
38
|
-
child.stdin.end(stdin);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe("parseNameStatus", () => {
|
|
43
|
-
it("parses added and modified paths from git diff --name-status output", () => {
|
|
44
|
-
const changes = parseNameStatus("M\tpackage.json\nA\t.changeset/fix-release.md\n");
|
|
45
|
-
|
|
46
|
-
assert.deepEqual(changes, [
|
|
47
|
-
{ status: "M", path: "package.json" },
|
|
48
|
-
{ status: "A", path: ".changeset/fix-release.md" },
|
|
49
|
-
]);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("uses the new path for a rename", () => {
|
|
53
|
-
const changes = parseNameStatus("R087\t.changeset/old.md\t.changeset/new.md\n");
|
|
54
|
-
|
|
55
|
-
assert.deepEqual(changes, [{ status: "R", path: ".changeset/new.md" }]);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("returns no changes for empty output", () => {
|
|
59
|
-
assert.deepEqual(parseNameStatus(""), []);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("evaluateChangesetGate", () => {
|
|
64
|
-
it("passes a PR that adds a changeset", () => {
|
|
65
|
-
const result = evaluateChangesetGate({
|
|
66
|
-
changes: [{ status: "A", path: ".changeset/fix-release.md" }],
|
|
67
|
-
labels: [],
|
|
68
|
-
headRef: "fix/release-gate",
|
|
69
|
-
actor: "jaybeeuu",
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
assert.deepEqual(result, { ok: true, reason: "changeset-added" });
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("fails a CI-only change that adds no changeset", () => {
|
|
76
|
-
const result = evaluateChangesetGate({
|
|
77
|
-
changes: [{ status: "M", path: ".github/workflows/ci.yml" }],
|
|
78
|
-
labels: [],
|
|
79
|
-
headRef: "chore/bump-pnpm",
|
|
80
|
-
actor: "jaybeeuu",
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
assert.equal(result.ok, false);
|
|
84
|
-
assert.equal(result.reason, "missing-changeset");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("fails a PR that only modifies an existing changeset", () => {
|
|
88
|
-
const result = evaluateChangesetGate({
|
|
89
|
-
changes: [{ status: "M", path: ".changeset/pending.md" }],
|
|
90
|
-
labels: [],
|
|
91
|
-
headRef: "chore/edit-changeset",
|
|
92
|
-
actor: "jaybeeuu",
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
assert.equal(result.ok, false);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("fails a PR that only adds the changeset README", () => {
|
|
99
|
-
const result = evaluateChangesetGate({
|
|
100
|
-
changes: [{ status: "A", path: ".changeset/README.md" }],
|
|
101
|
-
labels: [],
|
|
102
|
-
headRef: "docs/changesets",
|
|
103
|
-
actor: "jaybeeuu",
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
assert.equal(result.ok, false);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("passes an unlabelled-change PR carrying the no-publish label", () => {
|
|
110
|
-
const result = evaluateChangesetGate({
|
|
111
|
-
changes: [{ status: "M", path: ".github/workflows/ci.yml" }],
|
|
112
|
-
labels: ["no-publish"],
|
|
113
|
-
headRef: "chore/bump-pnpm",
|
|
114
|
-
actor: "jaybeeuu",
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
assert.deepEqual(result, { ok: true, reason: "no-publish-label" });
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("passes the Version Packages PR by head branch", () => {
|
|
121
|
-
const result = evaluateChangesetGate({
|
|
122
|
-
changes: [{ status: "M", path: "package.json" }],
|
|
123
|
-
labels: [],
|
|
124
|
-
headRef: "changeset-release/main",
|
|
125
|
-
actor: "jaybeeuu",
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
assert.deepEqual(result, { ok: true, reason: "version-packages-pr" });
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("passes a PR opened by the GitHub Actions bot", () => {
|
|
132
|
-
const result = evaluateChangesetGate({
|
|
133
|
-
changes: [{ status: "M", path: "package.json" }],
|
|
134
|
-
labels: [],
|
|
135
|
-
headRef: "automation/some-branch",
|
|
136
|
-
actor: "github-actions[bot]",
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
assert.deepEqual(result, { ok: true, reason: "version-packages-pr" });
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe("resolvePublishCommand", () => {
|
|
144
|
-
it("publishes by default when the merged PR has no labels", () => {
|
|
145
|
-
assert.equal(resolvePublishCommand([]), PUBLISH_COMMAND);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("publishes when the merged PR carries unrelated labels", () => {
|
|
149
|
-
assert.equal(resolvePublishCommand(["bug", "priority:p2"]), PUBLISH_COMMAND);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("refuses to publish when the merged PR carries the no-publish label", () => {
|
|
153
|
-
assert.equal(resolvePublishCommand(["bug", "no-publish"]), NO_PUBLISH_COMMAND);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("matches the label exactly, not as a substring", () => {
|
|
157
|
-
assert.equal(resolvePublishCommand(["no-publish-ci"]), PUBLISH_COMMAND);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe("check-changeset CLI", () => {
|
|
162
|
-
it("exits 0 for a no-publish PR with no diff", async () => {
|
|
163
|
-
const { code } = await runScript(CHECK_CHANGESET, {
|
|
164
|
-
env: { PR_LABELS: '["no-publish"]', PR_HEAD_REF: "chore/bump-pnpm", PR_ACTOR: "jaybeeuu" },
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
assert.equal(code, 0);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("exits 1 and explains the no-publish escape hatch for a changeset-less PR", async () => {
|
|
171
|
-
const { code, stderr } = await runScript(CHECK_CHANGESET, {
|
|
172
|
-
stdin: "M\tpackage.json\n",
|
|
173
|
-
env: { PR_LABELS: "[]", PR_HEAD_REF: "chore/bump-pnpm", PR_ACTOR: "jaybeeuu" },
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
assert.equal(code, 1);
|
|
177
|
-
assert.match(stderr, /no-publish/);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe("resolve-publish-command CLI", () => {
|
|
182
|
-
it("prints the noop command for a no-publish label set", async () => {
|
|
183
|
-
const { code, stdout } = await runScript(RESOLVE_PUBLISH, {
|
|
184
|
-
args: ['["no-publish"]'],
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
assert.equal(code, 0);
|
|
188
|
-
assert.equal(stdout.trim(), NO_PUBLISH_COMMAND);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// Behaviour tests for scripts/lib/skill-frontmatter.mjs — the SKILL.md
|
|
2
|
-
// frontmatter check that `pnpm lint` runs. Each case proves one failure mode
|
|
3
|
-
// the check must catch: unparseable YAML, a missing name/description field, and
|
|
4
|
-
// a description longer than the limit.
|
|
5
|
-
|
|
6
|
-
import { describe, it } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { dirname, join } from "node:path";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import {
|
|
13
|
-
MAX_DESCRIPTION_LENGTH,
|
|
14
|
-
checkSkills,
|
|
15
|
-
validateSkillFrontmatter,
|
|
16
|
-
} from "../scripts/lib/skill-frontmatter.mjs";
|
|
17
|
-
|
|
18
|
-
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
-
|
|
20
|
-
/** Wrap frontmatter lines in a SKILL.md-shaped document. */
|
|
21
|
-
function skillDocument(frontmatter) {
|
|
22
|
-
return `---\n${frontmatter}\n---\n\n# Skill body\n`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Throwaway directory to lay out a fake skills tree in. */
|
|
26
|
-
async function makeFixtureRoot() {
|
|
27
|
-
return await mkdtemp(join(tmpdir(), "agent-cortex-skill-frontmatter-"));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Write a SKILL.md at `<root>/<relativeSkillDir>/SKILL.md`. */
|
|
31
|
-
async function writeSkill(root, relativeSkillDir, content) {
|
|
32
|
-
const dir = join(root, relativeSkillDir);
|
|
33
|
-
await mkdir(dir, { recursive: true });
|
|
34
|
-
await writeFile(join(dir, "SKILL.md"), content);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("validateSkillFrontmatter", () => {
|
|
38
|
-
it("accepts frontmatter with a name and a description", () => {
|
|
39
|
-
const errors = validateSkillFrontmatter(
|
|
40
|
-
skillDocument("name: example-skill\ndescription: Does a thing. Use when a thing needs doing."),
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
assert.deepEqual(errors, []);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("reports unparseable YAML", () => {
|
|
47
|
-
const errors = validateSkillFrontmatter(
|
|
48
|
-
skillDocument("name: example-skill\ndescription: Use when the plan: breaks"),
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
assert.equal(errors.length, 1);
|
|
52
|
-
assert.match(errors[0], /not valid YAML/);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("reports a missing name", () => {
|
|
56
|
-
const errors = validateSkillFrontmatter(skillDocument("description: Does a thing."));
|
|
57
|
-
|
|
58
|
-
assert.deepEqual(errors, ["frontmatter is missing a non-empty name"]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("reports a missing description", () => {
|
|
62
|
-
const errors = validateSkillFrontmatter(skillDocument("name: example-skill"));
|
|
63
|
-
|
|
64
|
-
assert.deepEqual(errors, ["frontmatter is missing a non-empty description"]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("reports a description over the length limit", () => {
|
|
68
|
-
const errors = validateSkillFrontmatter(
|
|
69
|
-
skillDocument(`name: example-skill\ndescription: ${"a".repeat(MAX_DESCRIPTION_LENGTH + 1)}`),
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
assert.equal(errors.length, 1);
|
|
73
|
-
assert.match(errors[0], /description exceeds 1024 characters \(1025\)/);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("accepts a description exactly at the length limit", () => {
|
|
77
|
-
const errors = validateSkillFrontmatter(
|
|
78
|
-
skillDocument(`name: example-skill\ndescription: ${"a".repeat(MAX_DESCRIPTION_LENGTH)}`),
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
assert.deepEqual(errors, []);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("reports a file with no frontmatter block", () => {
|
|
85
|
-
const errors = validateSkillFrontmatter("# Skill body\n\nNo frontmatter here.\n");
|
|
86
|
-
|
|
87
|
-
assert.deepEqual(errors, ["frontmatter block is missing (expected '---' delimiters)"]);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe("checkSkills", () => {
|
|
92
|
-
it("reports each failing SKILL.md and counts every file it checked", async () => {
|
|
93
|
-
const root = await makeFixtureRoot();
|
|
94
|
-
try {
|
|
95
|
-
await writeSkill(
|
|
96
|
-
root,
|
|
97
|
-
"skills/good",
|
|
98
|
-
skillDocument("name: good-skill\ndescription: Does a thing. Use when testing the checker."),
|
|
99
|
-
);
|
|
100
|
-
await writeSkill(
|
|
101
|
-
root,
|
|
102
|
-
"skills/broken",
|
|
103
|
-
skillDocument("name: broken-skill\ndescription: Use when the plan: breaks"),
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const result = await checkSkills({ root });
|
|
107
|
-
|
|
108
|
-
assert.equal(result.checked, 2);
|
|
109
|
-
assert.equal(result.failures.length, 1);
|
|
110
|
-
assert.equal(result.failures[0].file, join("skills", "broken", "SKILL.md"));
|
|
111
|
-
} finally {
|
|
112
|
-
await rm(root, { recursive: true, force: true });
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("ignores markdown files that are not SKILL.md", async () => {
|
|
117
|
-
const root = await makeFixtureRoot();
|
|
118
|
-
try {
|
|
119
|
-
await writeSkill(root, "skills/good", skillDocument("name: good-skill\ndescription: Does a thing."));
|
|
120
|
-
await writeFile(join(root, "skills", "good", "REFERENCE.md"), "---\nnot: a skill\n---\n");
|
|
121
|
-
|
|
122
|
-
const result = await checkSkills({ root });
|
|
123
|
-
|
|
124
|
-
assert.equal(result.checked, 1);
|
|
125
|
-
assert.deepEqual(result.failures, []);
|
|
126
|
-
} finally {
|
|
127
|
-
await rm(root, { recursive: true, force: true });
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("passes on this repository's own skills tree", async () => {
|
|
132
|
-
const result = await checkSkills({ root: REPO_ROOT });
|
|
133
|
-
|
|
134
|
-
assert.deepEqual(result.failures, []);
|
|
135
|
-
assert.ok(result.checked > 0, "expected the check to find SKILL.md files");
|
|
136
|
-
});
|
|
137
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// Integration tests for scripts/lib/verify-worker-tree.mjs. The module exists to
|
|
2
|
-
// tell a worker that delivered from one that only said it did, so each case runs
|
|
3
|
-
// a real git repository and asserts the verdict — no mocking of git, because git
|
|
4
|
-
// is the thing whose state the verdict is about.
|
|
5
|
-
|
|
6
|
-
import { describe, it } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { execFile } from "node:child_process";
|
|
9
|
-
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { promisify } from "node:util";
|
|
13
|
-
|
|
14
|
-
import { verifyWorkerTree } from "../scripts/lib/verify-worker-tree.mjs";
|
|
15
|
-
|
|
16
|
-
const execFileAsync = promisify(execFile);
|
|
17
|
-
|
|
18
|
-
/** Run a git command in `cwd`. */
|
|
19
|
-
async function git(cwd, args) {
|
|
20
|
-
await execFileAsync("git", args, { cwd });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** A repository with one base commit; returns its directory and that commit. */
|
|
24
|
-
async function createRepo() {
|
|
25
|
-
const dir = await mkdtemp(join(tmpdir(), "verify-worker-tree-"));
|
|
26
|
-
await git(dir, ["init", "--quiet"]);
|
|
27
|
-
await git(dir, ["config", "user.email", "t@example.com"]);
|
|
28
|
-
await git(dir, ["config", "user.name", "T"]);
|
|
29
|
-
// A developer's global config may enable commit signing; the fixture must not
|
|
30
|
-
// depend on a secret key being present.
|
|
31
|
-
await git(dir, ["config", "commit.gpgsign", "false"]);
|
|
32
|
-
await writeFile(join(dir, "README.md"), "base\n");
|
|
33
|
-
await git(dir, ["add", "-A"]);
|
|
34
|
-
await git(dir, ["commit", "-m", "base"]);
|
|
35
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: dir });
|
|
36
|
-
return { dir, base: stdout.trim() };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Commit a file in the repo, as a delivering worker would. */
|
|
40
|
-
async function commitWork(dir) {
|
|
41
|
-
await writeFile(join(dir, "worker-output.txt"), "delivered\n");
|
|
42
|
-
await git(dir, ["add", "-A"]);
|
|
43
|
-
await git(dir, ["commit", "-m", "worker: deliver"]);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe("verifyWorkerTree", () => {
|
|
47
|
-
it("passes a worker that committed and left a clean tree", async () => {
|
|
48
|
-
const { dir, base } = await createRepo();
|
|
49
|
-
try {
|
|
50
|
-
await commitWork(dir);
|
|
51
|
-
|
|
52
|
-
const verdict = await verifyWorkerTree({ worktree: dir, base });
|
|
53
|
-
|
|
54
|
-
assert.deepEqual(verdict, { ok: true, reasons: [], commitsAhead: 1 });
|
|
55
|
-
} finally {
|
|
56
|
-
await rm(dir, { recursive: true, force: true });
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("fails a worker that reported success but left its work uncommitted", async () => {
|
|
61
|
-
const { dir, base } = await createRepo();
|
|
62
|
-
try {
|
|
63
|
-
await writeFile(join(dir, "left-behind.txt"), "uncommitted\n");
|
|
64
|
-
|
|
65
|
-
const verdict = await verifyWorkerTree({ worktree: dir, base });
|
|
66
|
-
|
|
67
|
-
assert.equal(verdict.ok, false);
|
|
68
|
-
assert.equal(verdict.reasons[0], "worktree is dirty: 1 uncommitted path(s)");
|
|
69
|
-
assert.equal(verdict.commitsAhead, 0);
|
|
70
|
-
} finally {
|
|
71
|
-
await rm(dir, { recursive: true, force: true });
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("fails a worker that left a clean tree with nothing committed", async () => {
|
|
76
|
-
const { dir, base } = await createRepo();
|
|
77
|
-
try {
|
|
78
|
-
const verdict = await verifyWorkerTree({ worktree: dir, base });
|
|
79
|
-
|
|
80
|
-
assert.equal(verdict.ok, false);
|
|
81
|
-
assert.equal(verdict.reasons.length, 1);
|
|
82
|
-
assert.match(verdict.reasons[0], /^no commit ahead of base [0-9a-f]{7}$/);
|
|
83
|
-
} finally {
|
|
84
|
-
await rm(dir, { recursive: true, force: true });
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("reports a dirty tree and a missing commit together", async () => {
|
|
89
|
-
const { dir, base } = await createRepo();
|
|
90
|
-
try {
|
|
91
|
-
await writeFile(join(dir, "partial.txt"), "half done\n");
|
|
92
|
-
|
|
93
|
-
const verdict = await verifyWorkerTree({ worktree: dir, base });
|
|
94
|
-
|
|
95
|
-
assert.equal(verdict.ok, false);
|
|
96
|
-
assert.equal(verdict.reasons.length, 2);
|
|
97
|
-
assert.equal(verdict.reasons[0], "worktree is dirty: 1 uncommitted path(s)");
|
|
98
|
-
assert.match(verdict.reasons[1], /^no commit ahead of base [0-9a-f]{7}$/);
|
|
99
|
-
} finally {
|
|
100
|
-
await rm(dir, { recursive: true, force: true });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("counts every commit the worker made, not just the last", async () => {
|
|
105
|
-
const { dir, base } = await createRepo();
|
|
106
|
-
try {
|
|
107
|
-
await commitWork(dir);
|
|
108
|
-
await writeFile(join(dir, "second.txt"), "more\n");
|
|
109
|
-
await git(dir, ["add", "-A"]);
|
|
110
|
-
await git(dir, ["commit", "-m", "worker: second"]);
|
|
111
|
-
|
|
112
|
-
const verdict = await verifyWorkerTree({ worktree: dir, base });
|
|
113
|
-
|
|
114
|
-
assert.equal(verdict.ok, true);
|
|
115
|
-
assert.equal(verdict.commitsAhead, 2);
|
|
116
|
-
} finally {
|
|
117
|
-
await rm(dir, { recursive: true, force: true });
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
});
|