@gempack/squad-mcp 0.5.0 → 0.6.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +3 -2
- package/CHANGELOG.md +271 -17
- package/INSTALL.md +156 -24
- package/README.md +278 -27
- package/agents/{PO.md → product-owner.md} +33 -1
- package/agents/{Senior-Architect.md → senior-architect.md} +33 -1
- package/agents/{Senior-DBA.md → senior-dba.md} +33 -1
- package/agents/{Senior-Dev-Reviewer.md → senior-dev-reviewer.md} +33 -1
- package/agents/{Senior-Dev-Security.md → senior-dev-security.md} +33 -1
- package/agents/{Senior-Developer.md → senior-developer.md} +33 -1
- package/agents/{Senior-QA.md → senior-qa.md} +33 -1
- package/agents/{TechLead-Consolidator.md → tech-lead-consolidator.md} +7 -1
- package/agents/{TechLead-Planner.md → tech-lead-planner.md} +7 -1
- package/commands/squad-review.md +10 -58
- package/commands/squad.md +11 -70
- package/dist/config/ownership-matrix.d.ts +24 -2
- package/dist/config/ownership-matrix.js +466 -139
- package/dist/config/ownership-matrix.js.map +1 -1
- package/dist/config/squad-yaml.d.ts +242 -0
- package/dist/config/squad-yaml.js +403 -0
- package/dist/config/squad-yaml.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/format/pr-review.d.ts +61 -0
- package/dist/format/pr-review.js +146 -0
- package/dist/format/pr-review.js.map +1 -0
- package/dist/index.js +19 -13
- package/dist/index.js.map +1 -1
- package/dist/learning/format.d.ts +29 -0
- package/dist/learning/format.js +55 -0
- package/dist/learning/format.js.map +1 -0
- package/dist/learning/store.d.ts +102 -0
- package/dist/learning/store.js +169 -0
- package/dist/learning/store.js.map +1 -0
- package/dist/resources/agent-loader.d.ts +8 -1
- package/dist/resources/agent-loader.js +83 -48
- package/dist/resources/agent-loader.js.map +1 -1
- package/dist/tasks/select.d.ts +64 -0
- package/dist/tasks/select.js +84 -0
- package/dist/tasks/select.js.map +1 -0
- package/dist/tasks/store.d.ts +338 -0
- package/dist/tasks/store.js +321 -0
- package/dist/tasks/store.js.map +1 -0
- package/dist/tools/compose-advisory-bundle.d.ts +5 -5
- package/dist/tools/compose-advisory-bundle.js +24 -12
- package/dist/tools/compose-advisory-bundle.js.map +1 -1
- package/dist/tools/compose-prd-parse.d.ts +53 -0
- package/dist/tools/compose-prd-parse.js +167 -0
- package/dist/tools/compose-prd-parse.js.map +1 -0
- package/dist/tools/compose-squad-workflow.d.ts +28 -10
- package/dist/tools/compose-squad-workflow.js +0 -0
- package/dist/tools/compose-squad-workflow.js.map +1 -1
- package/dist/tools/consolidate.d.ts +55 -4
- package/dist/tools/consolidate.js +87 -15
- package/dist/tools/consolidate.js.map +1 -1
- package/dist/tools/expand-task.d.ts +51 -0
- package/dist/tools/expand-task.js +35 -0
- package/dist/tools/expand-task.js.map +1 -0
- package/dist/tools/list-tasks.d.ts +31 -0
- package/dist/tools/list-tasks.js +50 -0
- package/dist/tools/list-tasks.js.map +1 -0
- package/dist/tools/next-task.d.ts +37 -0
- package/dist/tools/next-task.js +60 -0
- package/dist/tools/next-task.js.map +1 -0
- package/dist/tools/read-learnings.d.ts +53 -0
- package/dist/tools/read-learnings.js +72 -0
- package/dist/tools/read-learnings.js.map +1 -0
- package/dist/tools/read-squad-config.d.ts +23 -0
- package/dist/tools/read-squad-config.js +34 -0
- package/dist/tools/read-squad-config.js.map +1 -0
- package/dist/tools/record-learning.d.ts +62 -0
- package/dist/tools/record-learning.js +80 -0
- package/dist/tools/record-learning.js.map +1 -0
- package/dist/tools/record-tasks.d.ts +71 -0
- package/dist/tools/record-tasks.js +45 -0
- package/dist/tools/record-tasks.js.map +1 -0
- package/dist/tools/registry.d.ts +1 -1
- package/dist/tools/registry.js +71 -39
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/score-rubric.d.ts +74 -0
- package/dist/tools/score-rubric.js +140 -0
- package/dist/tools/score-rubric.js.map +1 -0
- package/dist/tools/slice-files-for-task.d.ts +31 -0
- package/dist/tools/slice-files-for-task.js +52 -0
- package/dist/tools/slice-files-for-task.js.map +1 -0
- package/dist/tools/update-task-status.d.ts +29 -0
- package/dist/tools/update-task-status.js +35 -0
- package/dist/tools/update-task-status.js.map +1 -0
- package/package.json +11 -1
- package/skills/squad/SKILL.md +454 -0
- package/tools/_tasks-io.mjs +69 -0
- package/tools/list-tasks.mjs +110 -0
- package/tools/next-task.mjs +131 -0
- package/tools/post-review.mjs +212 -0
- package/tools/record-learning.mjs +145 -0
- package/tools/record-tasks.mjs +186 -0
- package/tools/update-task-status.mjs +114 -0
- /package/{agents → shared}/Skill-Squad-Dev.md +0 -0
- /package/{agents → shared}/Skill-Squad-Review.md +0 -0
- /package/{agents → shared}/_Severity-and-Ownership.md +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Pick the next ready task: candidate status (default pending), all
|
|
3
|
+
// dependencies done, optional agent filter. Tiebreaker priority then id.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// tools/next-task.mjs [--agent senior-dba] [--workspace <path>]
|
|
7
|
+
// [--file <relpath>] [--json]
|
|
8
|
+
//
|
|
9
|
+
// Prints a one-line summary by default, or the full task as JSON with --json.
|
|
10
|
+
// If no ready task, prints reason ("no_candidates" / "all_blocked") and the
|
|
11
|
+
// blocked list.
|
|
12
|
+
//
|
|
13
|
+
// Exit codes:
|
|
14
|
+
// 0 ready task surfaced (or json mode, regardless of ready)
|
|
15
|
+
// 1 no ready task (text mode only — for shell pipelines)
|
|
16
|
+
// 2 invalid input
|
|
17
|
+
|
|
18
|
+
import { readTasksFile, fail } from "./_tasks-io.mjs";
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const PROG = "next-task";
|
|
22
|
+
const PRIORITY_RANK = { high: 0, medium: 1, low: 2 };
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const out = {
|
|
26
|
+
agent: null,
|
|
27
|
+
workspace: process.cwd(),
|
|
28
|
+
file: null,
|
|
29
|
+
json: false,
|
|
30
|
+
};
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const a = argv[i];
|
|
33
|
+
switch (a) {
|
|
34
|
+
case "--agent":
|
|
35
|
+
out.agent = argv[++i];
|
|
36
|
+
break;
|
|
37
|
+
case "--workspace":
|
|
38
|
+
out.workspace = argv[++i];
|
|
39
|
+
break;
|
|
40
|
+
case "--file":
|
|
41
|
+
out.file = argv[++i];
|
|
42
|
+
break;
|
|
43
|
+
case "--json":
|
|
44
|
+
out.json = true;
|
|
45
|
+
break;
|
|
46
|
+
case "--help":
|
|
47
|
+
case "-h":
|
|
48
|
+
process.stdout.write(
|
|
49
|
+
"usage: next-task.mjs [--agent NAME] [--workspace PATH] [--file PATH] [--json]\n",
|
|
50
|
+
);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
default:
|
|
53
|
+
fail(PROG, 2, `unknown flag: ${a}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pickNext(tasks, opts) {
|
|
60
|
+
const doneIds = new Set(
|
|
61
|
+
tasks.filter((t) => t.status === "done").map((t) => t.id),
|
|
62
|
+
);
|
|
63
|
+
let candidates = tasks.filter((t) => t.status === "pending");
|
|
64
|
+
if (opts.agent) {
|
|
65
|
+
candidates = candidates.filter(
|
|
66
|
+
(t) =>
|
|
67
|
+
!t.agent_hints ||
|
|
68
|
+
t.agent_hints.length === 0 ||
|
|
69
|
+
t.agent_hints.includes(opts.agent),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (candidates.length === 0) {
|
|
73
|
+
return { task: null, reason: "no_candidates", blocked: [] };
|
|
74
|
+
}
|
|
75
|
+
const ready = [];
|
|
76
|
+
const blocked = [];
|
|
77
|
+
for (const t of candidates) {
|
|
78
|
+
const missing = (t.dependencies ?? []).filter((d) => !doneIds.has(d));
|
|
79
|
+
if (missing.length === 0) {
|
|
80
|
+
ready.push(t);
|
|
81
|
+
} else {
|
|
82
|
+
blocked.push({ id: t.id, title: t.title, missing_deps: missing });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (ready.length === 0) {
|
|
86
|
+
return { task: null, reason: "all_blocked", blocked };
|
|
87
|
+
}
|
|
88
|
+
ready.sort((a, b) => {
|
|
89
|
+
const p =
|
|
90
|
+
PRIORITY_RANK[a.priority ?? "medium"] -
|
|
91
|
+
PRIORITY_RANK[b.priority ?? "medium"];
|
|
92
|
+
if (p !== 0) return p;
|
|
93
|
+
return a.id - b.id;
|
|
94
|
+
});
|
|
95
|
+
return { task: ready[0], reason: "ok", blocked };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function main() {
|
|
99
|
+
const opts = parseArgs(args);
|
|
100
|
+
const { data } = await readTasksFile(opts.workspace, opts.file);
|
|
101
|
+
const result = pickNext(data.tasks, opts);
|
|
102
|
+
|
|
103
|
+
if (opts.json) {
|
|
104
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (result.task) {
|
|
109
|
+
const t = result.task;
|
|
110
|
+
process.stdout.write(`#${t.id} [${t.priority ?? "medium"}] ${t.title}\n`);
|
|
111
|
+
if (t.scope) process.stdout.write(` scope: ${t.scope}\n`);
|
|
112
|
+
if (t.agent_hints && t.agent_hints.length > 0) {
|
|
113
|
+
process.stdout.write(` agents: ${t.agent_hints.join(", ")}\n`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (result.reason === "no_candidates") {
|
|
119
|
+
process.stderr.write("no pending tasks\n");
|
|
120
|
+
} else {
|
|
121
|
+
process.stderr.write("all candidates blocked:\n");
|
|
122
|
+
for (const b of result.blocked) {
|
|
123
|
+
process.stderr.write(
|
|
124
|
+
` #${b.id} ${b.title} (missing deps: ${b.missing_deps.join(", ")})\n`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().catch((err) => fail(PROG, 2, err.message ?? String(err)));
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Post a squad-mcp consolidation result as a `gh pr review` on a GitHub PR.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// echo '{...consolidator JSON...}' | node tools/post-review.mjs --pr 42
|
|
6
|
+
// echo '{...}' | node tools/post-review.mjs --pr 42 --dry-run
|
|
7
|
+
// echo '{...}' | node tools/post-review.mjs --pr 42 --request-changes-below 60
|
|
8
|
+
// echo '{...}' | node tools/post-review.mjs --pr 42 --repo owner/name
|
|
9
|
+
//
|
|
10
|
+
// Flags:
|
|
11
|
+
// --pr <number> PR number on the current repo (required)
|
|
12
|
+
// --repo <owner/name> Override the repo (else gh resolves from cwd)
|
|
13
|
+
// --request-changes-below <number> Force `request-changes` if APPROVED w/ score below this
|
|
14
|
+
// --dry-run Print the gh command + body, do NOT execute
|
|
15
|
+
// --no-footer Omit the "generated by squad-mcp" footer line
|
|
16
|
+
//
|
|
17
|
+
// Exit codes:
|
|
18
|
+
// 0 = success (posted, or dry-run produced output)
|
|
19
|
+
// 2 = invalid input / missing args
|
|
20
|
+
// 3 = gh not installed or not authenticated
|
|
21
|
+
// 4 = gh failed (non-zero) — prints stderr from gh
|
|
22
|
+
//
|
|
23
|
+
// This script lives outside the MCP server (tools/, alongside the commit-msg
|
|
24
|
+
// hook) because posting to GitHub is a side-effecting operation with auth.
|
|
25
|
+
// MCP tools are pure primitives. The skill SKILL.md orchestrates: it gets the
|
|
26
|
+
// consolidation JSON from `apply_consolidation_rules`, then invokes this CLI.
|
|
27
|
+
|
|
28
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
29
|
+
import { formatPrReview } from "../dist/format/pr-review.js";
|
|
30
|
+
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
|
|
33
|
+
function fail(code, msg) {
|
|
34
|
+
process.stderr.write(`post-review: ${msg}\n`);
|
|
35
|
+
process.exit(code);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const out = {
|
|
40
|
+
pr: null,
|
|
41
|
+
repo: null,
|
|
42
|
+
requestChangesBelow: undefined,
|
|
43
|
+
dryRun: false,
|
|
44
|
+
noFooter: false,
|
|
45
|
+
};
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const a = argv[i];
|
|
48
|
+
switch (a) {
|
|
49
|
+
case "--pr":
|
|
50
|
+
out.pr = argv[++i];
|
|
51
|
+
break;
|
|
52
|
+
case "--repo":
|
|
53
|
+
out.repo = argv[++i];
|
|
54
|
+
break;
|
|
55
|
+
case "--request-changes-below":
|
|
56
|
+
out.requestChangesBelow = Number(argv[++i]);
|
|
57
|
+
if (!Number.isFinite(out.requestChangesBelow)) {
|
|
58
|
+
fail(2, `--request-changes-below requires a number`);
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
case "--dry-run":
|
|
62
|
+
out.dryRun = true;
|
|
63
|
+
break;
|
|
64
|
+
case "--no-footer":
|
|
65
|
+
out.noFooter = true;
|
|
66
|
+
break;
|
|
67
|
+
case "--help":
|
|
68
|
+
case "-h":
|
|
69
|
+
process.stdout.write(
|
|
70
|
+
"usage: post-review.mjs --pr <n> [--repo owner/name] [--request-changes-below N] [--dry-run] [--no-footer]\n" +
|
|
71
|
+
"stdin: JSON output of apply_consolidation_rules\n",
|
|
72
|
+
);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
fail(2, `unknown flag: ${a}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!out.pr) fail(2, "--pr <number> is required");
|
|
80
|
+
if (!/^\d+$/.test(out.pr))
|
|
81
|
+
fail(2, `--pr must be a positive integer, got "${out.pr}"`);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function readStdin() {
|
|
86
|
+
return await new Promise((resolve, reject) => {
|
|
87
|
+
let data = "";
|
|
88
|
+
process.stdin.setEncoding("utf8");
|
|
89
|
+
process.stdin.on("data", (chunk) => {
|
|
90
|
+
data += chunk;
|
|
91
|
+
});
|
|
92
|
+
process.stdin.on("end", () => resolve(data));
|
|
93
|
+
process.stdin.on("error", reject);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ensureGh() {
|
|
98
|
+
// gh --version is read-only and fast. If it's not installed, error early
|
|
99
|
+
// with a clear message instead of letting the user discover it via spawn ENOENT.
|
|
100
|
+
const r = spawnSync("gh", ["--version"], { encoding: "utf8" });
|
|
101
|
+
if (r.error) {
|
|
102
|
+
if (r.error.code === "ENOENT") {
|
|
103
|
+
fail(
|
|
104
|
+
3,
|
|
105
|
+
"gh CLI not found in PATH. Install: https://cli.github.com/manual/installation",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
fail(3, `gh check failed: ${r.error.message}`);
|
|
109
|
+
}
|
|
110
|
+
if (r.status !== 0) {
|
|
111
|
+
fail(3, `gh --version exited ${r.status}: ${r.stderr || r.stdout}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function runGh(args, body) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const proc = spawn("gh", args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
118
|
+
let stdout = "";
|
|
119
|
+
let stderr = "";
|
|
120
|
+
proc.stdout.on("data", (d) => (stdout += d));
|
|
121
|
+
proc.stderr.on("data", (d) => (stderr += d));
|
|
122
|
+
proc.on("error", reject);
|
|
123
|
+
proc.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
124
|
+
proc.stdin.write(body);
|
|
125
|
+
proc.stdin.end();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function main() {
|
|
130
|
+
const opts = parseArgs(args);
|
|
131
|
+
|
|
132
|
+
let raw;
|
|
133
|
+
try {
|
|
134
|
+
raw = await readStdin();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
fail(2, `failed to read stdin: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
if (!raw || raw.trim() === "") {
|
|
139
|
+
fail(2, "no JSON received on stdin");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let consolidation;
|
|
143
|
+
try {
|
|
144
|
+
consolidation = JSON.parse(raw);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
fail(2, `invalid JSON on stdin: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
!consolidation ||
|
|
150
|
+
typeof consolidation !== "object" ||
|
|
151
|
+
!consolidation.verdict
|
|
152
|
+
) {
|
|
153
|
+
fail(
|
|
154
|
+
2,
|
|
155
|
+
"stdin JSON missing required `verdict` field — expected output of apply_consolidation_rules",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const formatOptions = {};
|
|
160
|
+
if (typeof opts.requestChangesBelow === "number") {
|
|
161
|
+
formatOptions.requestChangesBelowScore = opts.requestChangesBelow;
|
|
162
|
+
}
|
|
163
|
+
if (opts.repo) formatOptions.repoLabel = opts.repo;
|
|
164
|
+
|
|
165
|
+
const payload = formatPrReview(consolidation, formatOptions);
|
|
166
|
+
let body = payload.body;
|
|
167
|
+
if (opts.noFooter) {
|
|
168
|
+
// Strip the trailing "---\n_Generated by..._\n" footer block. Idempotent.
|
|
169
|
+
body = body.replace(/\n\n---\n[\s\S]*$/, "\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ghArgs = [
|
|
173
|
+
"pr",
|
|
174
|
+
"review",
|
|
175
|
+
opts.pr,
|
|
176
|
+
`--${payload.action}`,
|
|
177
|
+
"--body-file",
|
|
178
|
+
"-",
|
|
179
|
+
];
|
|
180
|
+
if (opts.repo) ghArgs.push("--repo", opts.repo);
|
|
181
|
+
|
|
182
|
+
if (opts.dryRun) {
|
|
183
|
+
process.stdout.write(`# DRY RUN — would execute:\n`);
|
|
184
|
+
process.stdout.write(
|
|
185
|
+
`gh ${ghArgs.map((a) => (a.includes(" ") ? JSON.stringify(a) : a)).join(" ")} <<'EOF'\n`,
|
|
186
|
+
);
|
|
187
|
+
process.stdout.write(body);
|
|
188
|
+
process.stdout.write(`EOF\n`);
|
|
189
|
+
process.stdout.write(
|
|
190
|
+
`\n# Action: ${payload.action}\n# Summary: ${payload.summary}\n`,
|
|
191
|
+
);
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
ensureGh();
|
|
196
|
+
const r = await runGh(ghArgs, body);
|
|
197
|
+
if (r.code !== 0) {
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
`gh ${payload.action} failed (exit ${r.code}):\n${r.stderr}`,
|
|
200
|
+
);
|
|
201
|
+
process.exit(4);
|
|
202
|
+
}
|
|
203
|
+
// gh prints the review URL on success; surface it to the caller.
|
|
204
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
205
|
+
process.stdout.write(
|
|
206
|
+
`\nposted: ${payload.action} on PR #${opts.pr} | ${payload.summary}\n`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
main().catch((err) => {
|
|
211
|
+
fail(4, `unexpected error: ${err && err.stack ? err.stack : err}`);
|
|
212
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Append a team decision (accept | reject) to `.squad/learnings.jsonl`.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// tools/record-learning.mjs --reject \
|
|
6
|
+
// --agent senior-dev-security \
|
|
7
|
+
// --finding "missing CSRF on POST /api/refund" \
|
|
8
|
+
// --reason "CSRF terminated at API gateway, see infra/edge.tf" \
|
|
9
|
+
// --pr 42
|
|
10
|
+
//
|
|
11
|
+
// tools/record-learning.mjs --accept \
|
|
12
|
+
// --agent senior-architect \
|
|
13
|
+
// --finding "cross-module coupling Auth -> Billing" \
|
|
14
|
+
// --reason "refactored to event bus" \
|
|
15
|
+
// --branch refactor/auth
|
|
16
|
+
//
|
|
17
|
+
// Flags:
|
|
18
|
+
// --accept | --reject (required, mutually exclusive)
|
|
19
|
+
// --agent <name> (required)
|
|
20
|
+
// --finding "<short title>" (required)
|
|
21
|
+
// --reason "<rationale>" (optional but recommended)
|
|
22
|
+
// --severity Blocker|Major|Minor|Suggestion (optional)
|
|
23
|
+
// --pr <number> (optional)
|
|
24
|
+
// --branch <name> (optional)
|
|
25
|
+
// --scope "<glob>" (optional, e.g. "src/auth/**")
|
|
26
|
+
// --workspace <path> (default: cwd)
|
|
27
|
+
// --file <relpath> (override the JSONL location for this run)
|
|
28
|
+
//
|
|
29
|
+
// Exit codes:
|
|
30
|
+
// 0 success
|
|
31
|
+
// 2 invalid input
|
|
32
|
+
|
|
33
|
+
import { promises as fs } from "node:fs";
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
|
|
38
|
+
function fail(code, msg) {
|
|
39
|
+
process.stderr.write(`record-learning: ${msg}\n`);
|
|
40
|
+
process.exit(code);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const out = {
|
|
45
|
+
decision: null, // 'accept' | 'reject'
|
|
46
|
+
agent: null,
|
|
47
|
+
finding: null,
|
|
48
|
+
reason: null,
|
|
49
|
+
severity: null,
|
|
50
|
+
pr: null,
|
|
51
|
+
branch: null,
|
|
52
|
+
scope: null,
|
|
53
|
+
workspace: process.cwd(),
|
|
54
|
+
file: null,
|
|
55
|
+
};
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
const a = argv[i];
|
|
58
|
+
switch (a) {
|
|
59
|
+
case "--accept":
|
|
60
|
+
if (out.decision)
|
|
61
|
+
fail(2, "--accept and --reject are mutually exclusive");
|
|
62
|
+
out.decision = "accept";
|
|
63
|
+
break;
|
|
64
|
+
case "--reject":
|
|
65
|
+
if (out.decision)
|
|
66
|
+
fail(2, "--accept and --reject are mutually exclusive");
|
|
67
|
+
out.decision = "reject";
|
|
68
|
+
break;
|
|
69
|
+
case "--agent":
|
|
70
|
+
out.agent = argv[++i];
|
|
71
|
+
break;
|
|
72
|
+
case "--finding":
|
|
73
|
+
out.finding = argv[++i];
|
|
74
|
+
break;
|
|
75
|
+
case "--reason":
|
|
76
|
+
out.reason = argv[++i];
|
|
77
|
+
break;
|
|
78
|
+
case "--severity":
|
|
79
|
+
out.severity = argv[++i];
|
|
80
|
+
break;
|
|
81
|
+
case "--pr":
|
|
82
|
+
out.pr = Number(argv[++i]);
|
|
83
|
+
if (!Number.isInteger(out.pr) || out.pr <= 0) {
|
|
84
|
+
fail(2, "--pr must be a positive integer");
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
case "--branch":
|
|
88
|
+
out.branch = argv[++i];
|
|
89
|
+
break;
|
|
90
|
+
case "--scope":
|
|
91
|
+
out.scope = argv[++i];
|
|
92
|
+
break;
|
|
93
|
+
case "--workspace":
|
|
94
|
+
out.workspace = argv[++i];
|
|
95
|
+
break;
|
|
96
|
+
case "--file":
|
|
97
|
+
out.file = argv[++i];
|
|
98
|
+
break;
|
|
99
|
+
case "--help":
|
|
100
|
+
case "-h":
|
|
101
|
+
process.stdout.write(
|
|
102
|
+
"usage: record-learning.mjs --accept|--reject --agent <name> --finding <title> [options]\n",
|
|
103
|
+
);
|
|
104
|
+
process.exit(0);
|
|
105
|
+
default:
|
|
106
|
+
fail(2, `unknown flag: ${a}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!out.decision) fail(2, "one of --accept or --reject is required");
|
|
110
|
+
if (!out.agent) fail(2, "--agent <name> is required");
|
|
111
|
+
if (!out.finding) fail(2, "--finding <title> is required");
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const opts = parseArgs(args);
|
|
117
|
+
const ts = new Date().toISOString();
|
|
118
|
+
const entry = {
|
|
119
|
+
ts,
|
|
120
|
+
agent: opts.agent,
|
|
121
|
+
finding: opts.finding,
|
|
122
|
+
decision: opts.decision,
|
|
123
|
+
};
|
|
124
|
+
if (opts.severity) entry.severity = opts.severity;
|
|
125
|
+
if (opts.reason) entry.reason = opts.reason;
|
|
126
|
+
if (opts.pr) entry.pr = opts.pr;
|
|
127
|
+
if (opts.branch) entry.branch = opts.branch;
|
|
128
|
+
if (opts.scope) entry.scope = opts.scope;
|
|
129
|
+
|
|
130
|
+
const target = path.resolve(
|
|
131
|
+
opts.workspace,
|
|
132
|
+
opts.file ?? ".squad/learnings.jsonl",
|
|
133
|
+
);
|
|
134
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
135
|
+
await fs.appendFile(target, JSON.stringify(entry) + "\n", "utf8");
|
|
136
|
+
|
|
137
|
+
process.stdout.write(
|
|
138
|
+
`recorded: ${opts.decision} on ${opts.agent} — "${opts.finding}"\n`,
|
|
139
|
+
);
|
|
140
|
+
process.stdout.write(`file: ${target}\n`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main().catch((err) => {
|
|
144
|
+
fail(2, `unexpected error: ${err && err.stack ? err.stack : err}`);
|
|
145
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bulk-add tasks to `.squad/tasks.json`. Reads a JSON array of task inputs
|
|
3
|
+
// from stdin (or a file via --input) and appends them.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// echo '[{"title":"Add CSRF","scope":"src/api/**"}]' | tools/record-tasks.mjs
|
|
7
|
+
// tools/record-tasks.mjs --input parsed-prd.json
|
|
8
|
+
//
|
|
9
|
+
// Each task input may include: title (required), description, dependencies,
|
|
10
|
+
// priority, details, test_strategy, scope, agent_hints. id is optional —
|
|
11
|
+
// auto-allocated as max(existing) + 1 in input order.
|
|
12
|
+
//
|
|
13
|
+
// Flags:
|
|
14
|
+
// --input <path> Read JSON from this file instead of stdin
|
|
15
|
+
// --workspace <path> Default: cwd
|
|
16
|
+
// --file <relpath> Override the JSON store location
|
|
17
|
+
// --dry-run Validate + print resulting file, do not write
|
|
18
|
+
//
|
|
19
|
+
// Exit codes:
|
|
20
|
+
// 0 success
|
|
21
|
+
// 2 invalid input
|
|
22
|
+
//
|
|
23
|
+
// This CLI is intentionally minimal — no schema validation beyond shape.
|
|
24
|
+
// Production use should go through the MCP `record_tasks` tool which
|
|
25
|
+
// validates the full zod schema.
|
|
26
|
+
|
|
27
|
+
import { promises as fs } from "node:fs";
|
|
28
|
+
import {
|
|
29
|
+
readTasksFile,
|
|
30
|
+
writeTasksFile,
|
|
31
|
+
VALID_PRIORITIES,
|
|
32
|
+
fail,
|
|
33
|
+
} from "./_tasks-io.mjs";
|
|
34
|
+
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const PROG = "record-tasks";
|
|
37
|
+
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const out = {
|
|
40
|
+
input: null,
|
|
41
|
+
workspace: process.cwd(),
|
|
42
|
+
file: null,
|
|
43
|
+
dryRun: false,
|
|
44
|
+
};
|
|
45
|
+
for (let i = 0; i < argv.length; i++) {
|
|
46
|
+
const a = argv[i];
|
|
47
|
+
switch (a) {
|
|
48
|
+
case "--input":
|
|
49
|
+
out.input = argv[++i];
|
|
50
|
+
break;
|
|
51
|
+
case "--workspace":
|
|
52
|
+
out.workspace = argv[++i];
|
|
53
|
+
break;
|
|
54
|
+
case "--file":
|
|
55
|
+
out.file = argv[++i];
|
|
56
|
+
break;
|
|
57
|
+
case "--dry-run":
|
|
58
|
+
out.dryRun = true;
|
|
59
|
+
break;
|
|
60
|
+
case "--help":
|
|
61
|
+
case "-h":
|
|
62
|
+
process.stdout.write(
|
|
63
|
+
"usage: record-tasks.mjs [--input PATH | <stdin>] [--workspace PATH] [--file PATH] [--dry-run]\n",
|
|
64
|
+
);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
default:
|
|
67
|
+
fail(PROG, 2, `unknown flag: ${a}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readStdin() {
|
|
74
|
+
const chunks = [];
|
|
75
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
76
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateInputs(inputs) {
|
|
80
|
+
if (!Array.isArray(inputs) || inputs.length === 0) {
|
|
81
|
+
fail(PROG, 2, "input must be a non-empty array of task objects");
|
|
82
|
+
}
|
|
83
|
+
for (const [i, t] of inputs.entries()) {
|
|
84
|
+
if (!t || typeof t !== "object") {
|
|
85
|
+
fail(PROG, 2, `input[${i}]: not an object`);
|
|
86
|
+
}
|
|
87
|
+
if (typeof t.title !== "string" || t.title.length === 0) {
|
|
88
|
+
fail(PROG, 2, `input[${i}]: title is required and must be a string`);
|
|
89
|
+
}
|
|
90
|
+
if (t.priority !== undefined && !VALID_PRIORITIES.includes(t.priority)) {
|
|
91
|
+
fail(PROG, 2, `input[${i}]: priority must be low|medium|high`);
|
|
92
|
+
}
|
|
93
|
+
if (t.dependencies !== undefined && !Array.isArray(t.dependencies)) {
|
|
94
|
+
fail(PROG, 2, `input[${i}]: dependencies must be an array`);
|
|
95
|
+
}
|
|
96
|
+
if (t.id !== undefined && (!Number.isInteger(t.id) || t.id <= 0)) {
|
|
97
|
+
fail(PROG, 2, `input[${i}]: id must be a positive integer`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
const opts = parseArgs(args);
|
|
104
|
+
const raw = opts.input
|
|
105
|
+
? await fs.readFile(opts.input, "utf8")
|
|
106
|
+
: await readStdin();
|
|
107
|
+
|
|
108
|
+
let inputs;
|
|
109
|
+
try {
|
|
110
|
+
inputs = JSON.parse(raw);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
fail(PROG, 2, `invalid JSON on input: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
validateInputs(inputs);
|
|
115
|
+
|
|
116
|
+
const { filePath, data } = await readTasksFile(opts.workspace, opts.file);
|
|
117
|
+
const existingIds = new Set(data.tasks.map((t) => t.id));
|
|
118
|
+
let cursor = data.tasks.reduce((m, t) => Math.max(m, t.id), 0);
|
|
119
|
+
const ts = new Date().toISOString();
|
|
120
|
+
const newTasks = [];
|
|
121
|
+
const seen = new Set(existingIds);
|
|
122
|
+
|
|
123
|
+
for (const inp of inputs) {
|
|
124
|
+
let id = inp.id;
|
|
125
|
+
if (id === undefined) {
|
|
126
|
+
cursor += 1;
|
|
127
|
+
id = cursor;
|
|
128
|
+
} else {
|
|
129
|
+
if (seen.has(id)) {
|
|
130
|
+
fail(PROG, 2, `duplicate task id ${id}`);
|
|
131
|
+
}
|
|
132
|
+
cursor = Math.max(cursor, id);
|
|
133
|
+
}
|
|
134
|
+
seen.add(id);
|
|
135
|
+
newTasks.push({
|
|
136
|
+
id,
|
|
137
|
+
title: inp.title,
|
|
138
|
+
...(inp.description !== undefined && { description: inp.description }),
|
|
139
|
+
status: "pending",
|
|
140
|
+
dependencies: inp.dependencies ?? [],
|
|
141
|
+
priority: inp.priority ?? "medium",
|
|
142
|
+
...(inp.details !== undefined && { details: inp.details }),
|
|
143
|
+
...(inp.test_strategy !== undefined && {
|
|
144
|
+
test_strategy: inp.test_strategy,
|
|
145
|
+
}),
|
|
146
|
+
...(inp.scope !== undefined && { scope: inp.scope }),
|
|
147
|
+
...(inp.agent_hints !== undefined && { agent_hints: inp.agent_hints }),
|
|
148
|
+
subtasks: [],
|
|
149
|
+
created_at: ts,
|
|
150
|
+
updated_at: ts,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validate deps after id allocation (forward refs allowed in batch).
|
|
155
|
+
for (const t of newTasks) {
|
|
156
|
+
for (const dep of t.dependencies) {
|
|
157
|
+
if (!seen.has(dep)) {
|
|
158
|
+
fail(PROG, 2, `task ${t.id} depends on unknown id ${dep}`);
|
|
159
|
+
}
|
|
160
|
+
if (dep === t.id) {
|
|
161
|
+
fail(PROG, 2, `task ${t.id} cannot depend on itself`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const next = {
|
|
167
|
+
version: data.version ?? 1,
|
|
168
|
+
tasks: [...data.tasks, ...newTasks],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (opts.dryRun) {
|
|
172
|
+
process.stdout.write(JSON.stringify(next, null, 2) + "\n");
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
`would record: ${newTasks.length} task(s), ids ${newTasks.map((t) => t.id).join(", ")}\n`,
|
|
175
|
+
);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await writeTasksFile(filePath, next);
|
|
180
|
+
process.stdout.write(
|
|
181
|
+
`recorded: ${newTasks.length} task(s), ids ${newTasks.map((t) => t.id).join(", ")}\n`,
|
|
182
|
+
);
|
|
183
|
+
process.stdout.write(`file: ${filePath}\n`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main().catch((err) => fail(PROG, 2, err.message ?? String(err)));
|