@massa-ai/cursor-plugin 1.21.0 → 1.22.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/.cursor-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/massa-ai/references/evidence-gate.md +1 -1
- package/skills/massa-ai/references/hook-enforcement.md +2 -2
- package/skills/massa-ai/references/implementation-delivery.md +3 -3
- package/skills/massa-ai/references/lessons.md +9 -10
- package/skills/massa-ai/references/mcp-tools.md +1 -1
- package/skills/massa-ai/references/project-context.md +1 -1
- package/skills/massa-ai/references/spec-driven/artifact-store.md +7 -8
- package/skills/massa-ai/references/spec-driven/design.md +1 -1
- package/skills/massa-ai/references/spec-driven/execute.md +5 -5
- package/skills/massa-ai/references/spec-driven/specify.md +6 -6
- package/skills/massa-ai/references/spec-driven/sub-agents.md +1 -1
- package/skills/massa-ai/references/spec-driven/tasks.md +2 -2
- package/skills/massa-ai/references/spec-driven/validate.md +3 -3
- package/skills/massa-ai/scripts/check_commit.ts +231 -0
- package/skills/massa-ai/scripts/check_specs_delivered.ts +209 -0
- package/skills/massa-ai/scripts/lessons.ts +907 -0
- package/skills/massa-ai/scripts/validate_spec.ts +413 -0
- package/skills/massa-ai/scripts/validate_state.ts +276 -0
- package/skills/massa-ai/scripts/validate_tasks.ts +498 -0
- package/skills/massa-ai/workflows/architecture/architecture-fix.md +1 -1
- package/skills/massa-ai/workflows/bugs/bugs-fix.md +1 -1
- package/skills/massa-ai/workflows/code-quality/code-quality-fix.md +1 -1
- package/skills/massa-ai/workflows/debug.md +1 -1
- package/skills/massa-ai/workflows/feature.md +1 -1
- package/skills/massa-ai/workflows/general.md +2 -2
- package/skills/massa-ai/workflows/implementation/implementation-fix.md +1 -1
- package/skills/massa-ai/workflows/maestro/maestro-fix.md +1 -1
- package/skills/massa-ai/workflows/mobile-figma/mobile-figma-fix.md +1 -1
- package/skills/massa-ai/workflows/refactor.md +1 -1
- package/skills/massa-ai/workflows/requirements/requirements-fix.md +1 -1
- package/skills/massa-ai/workflows/security/security-fix.md +1 -1
- package/skills/massa-ai/workflows/spec-driven.md +3 -3
- package/skills/massa-ai/workflows/tests/tests-fix.md +1 -1
- package/skills/massa-ai/scripts/check_commit.py +0 -128
- package/skills/massa-ai/scripts/check_specs_delivered.py +0 -137
- package/skills/massa-ai/scripts/lessons.py +0 -630
- package/skills/massa-ai/scripts/validate_spec.py +0 -272
- package/skills/massa-ai/scripts/validate_state.py +0 -183
- package/skills/massa-ai/scripts/validate_tasks.py +0 -302
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* check_specs_delivered.ts - deterministic gate: .specs/ artifacts are
|
|
4
|
+
* committed on the branch before a PR is opened (GATE-02).
|
|
5
|
+
*
|
|
6
|
+
* massa-ai's `implementation-delivery.md` chain has no gate requiring
|
|
7
|
+
* `.specs/` (spec/context/design/tasks/validation, project state, handoff,
|
|
8
|
+
* features registry) to be committed before `gh pr create`. This turns that
|
|
9
|
+
* gap into two conjunctive, deterministic checks:
|
|
10
|
+
*
|
|
11
|
+
* 1. `git status --porcelain -- .specs/` is empty - nothing under .specs/ is
|
|
12
|
+
* modified-but-uncommitted or untracked.
|
|
13
|
+
* 2. The feature's `spec.md` (always required) plus any of
|
|
14
|
+
* `{context,design,tasks,validation}.md` that exist on disk, plus
|
|
15
|
+
* `.specs/project/STATE.md`, `.specs/HANDOFF.md`, and
|
|
16
|
+
* `.specs/project/FEATURES.json`, are tracked on HEAD
|
|
17
|
+
* (`git ls-tree -r --name-only HEAD`).
|
|
18
|
+
*
|
|
19
|
+
* Check 2 exists because check 1 alone is not sufficient: a feature whose
|
|
20
|
+
* `.specs/` artifacts were simply never written is porcelain-clean (nothing to
|
|
21
|
+
* be dirty about) while still failing the actual requirement. Absence must fail,
|
|
22
|
+
* not pass.
|
|
23
|
+
*
|
|
24
|
+
* Bun builtins only, no new dependencies (D2 - `Bun.spawnSync` for the git
|
|
25
|
+
* calls). Run from the project root (the dir that contains .specs/), or pass
|
|
26
|
+
* --root.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* bun skills/massa-ai/scripts/check_specs_delivered.ts <feature> [--root DIR]
|
|
30
|
+
*
|
|
31
|
+
* Exit codes: 0 all required paths clean + tracked, 1 a required path is dirty,
|
|
32
|
+
* untracked, or not tracked on HEAD (paths named), 2 usage/git error.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { existsSync, statSync } from "node:fs";
|
|
36
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
37
|
+
|
|
38
|
+
// Always required for the named feature.
|
|
39
|
+
const FEATURE_REQUIRED = ["spec.md"];
|
|
40
|
+
// Required only when present on disk (not every feature reaches every phase).
|
|
41
|
+
const FEATURE_OPTIONAL = ["context.md", "design.md", "tasks.md", "validation.md"];
|
|
42
|
+
|
|
43
|
+
const STATE_FILES = [
|
|
44
|
+
join(".specs", "project", "STATE.md"),
|
|
45
|
+
join(".specs", "HANDOFF.md"),
|
|
46
|
+
join(".specs", "project", "FEATURES.json"),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Mirrors Python's str.splitlines(): universal newline split, no trailing empty element. */
|
|
50
|
+
function splitLines(text: string): string[] {
|
|
51
|
+
if (text === "") return [];
|
|
52
|
+
const result: string[] = [];
|
|
53
|
+
const lineBreakRe = /\r\n|\r|\n/g;
|
|
54
|
+
let start = 0;
|
|
55
|
+
let match: RegExpExecArray | null;
|
|
56
|
+
while ((match = lineBreakRe.exec(text)) !== null) {
|
|
57
|
+
result.push(text.slice(start, match.index));
|
|
58
|
+
start = match.index + match[0].length;
|
|
59
|
+
}
|
|
60
|
+
if (start < text.length) {
|
|
61
|
+
result.push(text.slice(start));
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runGit(args: string[], root: string): string {
|
|
67
|
+
let proc: ReturnType<typeof Bun.spawnSync>;
|
|
68
|
+
try {
|
|
69
|
+
proc = Bun.spawnSync(["git", ...args], { cwd: root, stdout: "pipe", stderr: "pipe" });
|
|
70
|
+
} catch {
|
|
71
|
+
console.error("check_specs_delivered: git not found on PATH");
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
const exitCode = proc.exitCode ?? -1;
|
|
75
|
+
if (exitCode !== 0) {
|
|
76
|
+
const stderrText = proc.stderr.toString().trim();
|
|
77
|
+
console.error(`check_specs_delivered: git ${args.join(" ")} failed: ${stderrText}`);
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
return proc.stdout.toString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Lines from `git status --porcelain -- .specs/` (empty = clean). */
|
|
84
|
+
function porcelainDirtyPaths(root: string): string[] {
|
|
85
|
+
const out = runGit(["status", "--porcelain", "--", ".specs/"], root);
|
|
86
|
+
return splitLines(out).filter((ln) => ln.trim() !== "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Set of every path tracked on HEAD, repo-root-relative, '/'-separated. */
|
|
90
|
+
function trackedOnHead(root: string): Set<string> {
|
|
91
|
+
const out = runGit(["ls-tree", "-r", "--name-only", "HEAD"], root);
|
|
92
|
+
return new Set(splitLines(out));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveFeatureDir(root: string, feature: string): string {
|
|
96
|
+
if (isAbsolute(feature) || feature.includes(sep)) {
|
|
97
|
+
return resolve(feature);
|
|
98
|
+
}
|
|
99
|
+
return join(root, ".specs", "features", feature);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Repo-root-relative, '/'-separated paths this feature must have tracked. */
|
|
103
|
+
function requiredPaths(root: string, feature: string): string[] {
|
|
104
|
+
const fdir = resolveFeatureDir(root, feature);
|
|
105
|
+
const fdirRel = relative(root, fdir);
|
|
106
|
+
const paths = FEATURE_REQUIRED.map((name) => join(fdirRel, name));
|
|
107
|
+
if (existsSync(fdir) && statSync(fdir).isDirectory()) {
|
|
108
|
+
for (const name of FEATURE_OPTIONAL) {
|
|
109
|
+
const candidate = join(fdir, name);
|
|
110
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
111
|
+
paths.push(join(fdirRel, name));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
paths.push(...STATE_FILES);
|
|
116
|
+
return paths.map((p) => p.split(sep).join("/"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Return { errors, checked }. errors empty = pass. */
|
|
120
|
+
function check(root: string, feature: string): { errors: string[]; checked: string[] } {
|
|
121
|
+
const errors: string[] = [];
|
|
122
|
+
|
|
123
|
+
for (const ln of porcelainDirtyPaths(root)) {
|
|
124
|
+
errors.push(`uncommitted/untracked under .specs/: ${ln.trim()}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const paths = requiredPaths(root, feature);
|
|
128
|
+
const tracked = trackedOnHead(root);
|
|
129
|
+
for (const p of paths) {
|
|
130
|
+
if (!tracked.has(p)) {
|
|
131
|
+
errors.push(`not tracked on HEAD: ${p}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { errors, checked: paths };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const USAGE = "usage: check_specs_delivered.ts [-h] [--root ROOT] feature";
|
|
139
|
+
const HELP = `${USAGE}
|
|
140
|
+
|
|
141
|
+
Gate: .specs/ artifacts committed on the branch before PR (GATE-02).
|
|
142
|
+
|
|
143
|
+
positional arguments:
|
|
144
|
+
feature Feature slug under <root>/.specs/features/, or a direct path
|
|
145
|
+
|
|
146
|
+
options:
|
|
147
|
+
-h, --help show this help message and exit
|
|
148
|
+
--root ROOT Project root containing .specs/ (default: current dir)`;
|
|
149
|
+
|
|
150
|
+
interface Args {
|
|
151
|
+
feature: string;
|
|
152
|
+
root: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function printUsageError(msg: string): void {
|
|
156
|
+
process.stderr.write(`${USAGE}\ncheck_specs_delivered.ts: error: ${msg}\n`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseArgs(argv: string[]): Args | null {
|
|
160
|
+
let root = ".";
|
|
161
|
+
const positionals: string[] = [];
|
|
162
|
+
for (let i = 0; i < argv.length; i++) {
|
|
163
|
+
const a = argv[i]!;
|
|
164
|
+
if (a === "--root") {
|
|
165
|
+
if (i + 1 >= argv.length) {
|
|
166
|
+
printUsageError("argument --root: expected one argument");
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
root = argv[++i]!;
|
|
170
|
+
} else if (a.startsWith("--root=")) {
|
|
171
|
+
root = a.slice("--root=".length);
|
|
172
|
+
} else if (a === "-h" || a === "--help") {
|
|
173
|
+
console.log(HELP);
|
|
174
|
+
process.exit(0);
|
|
175
|
+
} else if (a.startsWith("-") && a !== "-") {
|
|
176
|
+
printUsageError(`unrecognized arguments: ${a}`);
|
|
177
|
+
return null;
|
|
178
|
+
} else {
|
|
179
|
+
positionals.push(a);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (positionals.length === 0) {
|
|
183
|
+
printUsageError("the following arguments are required: feature");
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (positionals.length > 1) {
|
|
187
|
+
printUsageError(`unrecognized arguments: ${positionals.slice(1).join(" ")}`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return { feature: positionals[0]!, root };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function main(argv: string[]): number {
|
|
194
|
+
const args = parseArgs(argv);
|
|
195
|
+
if (args === null) return 2;
|
|
196
|
+
const root = resolve(args.root);
|
|
197
|
+
|
|
198
|
+
const { errors, checked } = check(root, args.feature);
|
|
199
|
+
|
|
200
|
+
for (const e of errors) console.log(` ERROR ${e}`);
|
|
201
|
+
console.log(`\ncheck_specs_delivered: checked ${checked.length} path(s):`);
|
|
202
|
+
for (const c of checked) console.log(` - ${c}`);
|
|
203
|
+
console.log(`check_specs_delivered: ${errors.length} error(s)`);
|
|
204
|
+
return errors.length ? 1 : 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (import.meta.main) {
|
|
208
|
+
process.exit(main(process.argv.slice(2)));
|
|
209
|
+
}
|