@syedshoaib/agent-ready 0.0.2
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/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/adapters/file.d.ts +2 -0
- package/dist/adapters/file.js +15 -0
- package/dist/adapters/github.d.ts +2 -0
- package/dist/adapters/github.js +70 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +97 -0
- package/dist/lint.d.ts +5 -0
- package/dist/lint.js +36 -0
- package/dist/render/markdown.d.ts +3 -0
- package/dist/render/markdown.js +28 -0
- package/dist/rules/built-in.d.ts +3 -0
- package/dist/rules/built-in.js +198 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
- package/rule-packs/default.yaml +50 -0
- package/schema/output.schema.json +43 -0
- package/schema/rule-pack.schema.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agent-ready contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# agent-ready
|
|
2
|
+
|
|
3
|
+
> Make every ticket ready for AI coding agents.
|
|
4
|
+
|
|
5
|
+
`agent-ready` is a Definition-of-Ready linter that runs **before** an AI coding agent picks up a ticket. It answers one question: *is this issue actually ready for an agent to work on?*
|
|
6
|
+
|
|
7
|
+
If the answer is no, it tells you exactly what's missing — in seconds, in CI, in the PR comment, before any tokens are spent.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
$ npx @syedshoaib/agent-ready check examples/tickets/bad-ticket.json
|
|
11
|
+
|
|
12
|
+
✗ PROJ-1234 not ready (4 blocker(s), 6 warning(s))
|
|
13
|
+
|
|
14
|
+
✗ has-acceptance-criteria No acceptance criteria found (need at least 1)
|
|
15
|
+
⚠ has-definition-of-done No Definition of Done found
|
|
16
|
+
✗ has-repo-target Ticket does not specify the target repo
|
|
17
|
+
✗ has-risk-classification No risk classification label
|
|
18
|
+
⚠ has-test-expectations No test expectations described
|
|
19
|
+
⚠ no-ambiguous-verbs Ambiguous verb(s): improve, make it better
|
|
20
|
+
✗ body-min-length Body too short: 91 chars (need >= 100)
|
|
21
|
+
⚠ no-tribal-knowledge Tribal-knowledge phrase(s): you know what i mean
|
|
22
|
+
⚠ t-shirt-size-present No t-shirt size estimate
|
|
23
|
+
⚠ has-design-link UI ticket has no design link
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
$ npx @syedshoaib/agent-ready check examples/tickets/good-ticket.json
|
|
28
|
+
|
|
29
|
+
✓ PROJ-2042 ready (10 checks passed)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Why this exists
|
|
33
|
+
|
|
34
|
+
Every team adopting Copilot, Cursor, Claude Code, or Codex agents hits the same wall:
|
|
35
|
+
|
|
36
|
+
> **Garbage tickets → garbage PRs.**
|
|
37
|
+
|
|
38
|
+
Agents are confident and fast. Without a clear ticket, that's a liability — they invent context, miss the real requirement, and silently burn tokens.
|
|
39
|
+
|
|
40
|
+
`agent-ready` is the cheap, automated gate that catches this. It runs in 50ms, has zero infrastructure, and plugs into Issue templates and PR workflows everyone already uses.
|
|
41
|
+
|
|
42
|
+
It's the **front door** of the agentic SDLC: prove the ticket is ready before any agent touches it.
|
|
43
|
+
|
|
44
|
+
## What it checks (v0 default rule pack — 10 rules)
|
|
45
|
+
|
|
46
|
+
| Rule | What it looks for |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `has-acceptance-criteria` | At least N acceptance criteria (numbered list, checklist, or Given/When) |
|
|
49
|
+
| `has-definition-of-done` | A DoD section in the body |
|
|
50
|
+
| `has-repo-target` | `repo:` in the body or a `repo:<name>` label |
|
|
51
|
+
| `has-risk-classification` | A `risk:low`/`risk:medium`/`risk:high` label |
|
|
52
|
+
| `has-design-link` | Figma/Ardoq/Miro/Excalidraw link present when the ticket has a `ui`/`ux`/`frontend` label |
|
|
53
|
+
| `has-test-expectations` | "How to verify" / test plan / Playwright / Jest / Pytest mentioned |
|
|
54
|
+
| `no-ambiguous-verbs` | Flags vague verbs (`improve`, `optimize`, `clean up`, `refactor`, `enhance`, …) |
|
|
55
|
+
| `body-min-length` | Body is at least 100 characters (configurable) |
|
|
56
|
+
| `no-tribal-knowledge` | Flags phrases like "as discussed", "you know what I mean", "the usual way" |
|
|
57
|
+
| `t-shirt-size-present` | `size:` in the body or a `size:S|M|L|XL` label |
|
|
58
|
+
|
|
59
|
+
Plus user-defined custom rules of `type: regex` (see [Rule pack format](#rule-pack-format)).
|
|
60
|
+
|
|
61
|
+
Every rule can be enabled, disabled, or tuned in a YAML rule pack.
|
|
62
|
+
|
|
63
|
+
> **Planned for v0.1 (not yet implemented):** `links-resolve`, `restricted-paths-declared`, plus `path_recommendation` / `context_tier` / `risk_classification` output fields, Jira/Linear adapters, SARIF output, an `agent-ready` label setter, and a Node plugin loader for custom rules.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# One-off use with a local ticket file
|
|
69
|
+
npx @syedshoaib/agent-ready check <path-to-ticket-json>
|
|
70
|
+
|
|
71
|
+
# Or fetch a real GitHub Issue
|
|
72
|
+
npx @syedshoaib/agent-ready check owner/repo#123 --adapter github
|
|
73
|
+
|
|
74
|
+
# Or install globally
|
|
75
|
+
npm i -g @syedshoaib/agent-ready
|
|
76
|
+
agent-ready check ./ticket.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> The CLI supports local JSON tickets and GitHub Issues. GitHub auth uses `GITHUB_TOKEN`, `GH_TOKEN`, or `gh auth token`. Native Jira/Linear CLI adapters are planned for v0.1.
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Lint a ticket from a local JSON file
|
|
85
|
+
agent-ready check examples/tickets/bad-ticket.json
|
|
86
|
+
agent-ready check examples/tickets/good-ticket.json
|
|
87
|
+
|
|
88
|
+
# Lint a GitHub Issue
|
|
89
|
+
agent-ready check Schoaib/agent-ready#1 --adapter github
|
|
90
|
+
agent-ready check https://github.com/Schoaib/agent-ready/issues/1 --adapter github
|
|
91
|
+
|
|
92
|
+
# Use a custom rule pack
|
|
93
|
+
agent-ready check ./ticket.json --rules ./my-rules.yaml
|
|
94
|
+
|
|
95
|
+
# Output formats
|
|
96
|
+
agent-ready check ./ticket.json --format text # default
|
|
97
|
+
agent-ready check ./ticket.json --format markdown # PR comment
|
|
98
|
+
agent-ready check ./ticket.json --format json # machine-readable
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Exit codes: `0` ready · `1` not ready · `2` usage error.
|
|
102
|
+
|
|
103
|
+
## GitHub Action
|
|
104
|
+
|
|
105
|
+
Drop this into `.github/workflows/agent-ready.yml`:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
name: Agent-Ready Check
|
|
109
|
+
on:
|
|
110
|
+
issues:
|
|
111
|
+
types: [opened, edited, labeled]
|
|
112
|
+
|
|
113
|
+
jobs:
|
|
114
|
+
check:
|
|
115
|
+
runs-on: ubuntu-latest
|
|
116
|
+
permissions:
|
|
117
|
+
contents: read
|
|
118
|
+
issues: write
|
|
119
|
+
steps:
|
|
120
|
+
- uses: actions/checkout@v6
|
|
121
|
+
- uses: Schoaib/agent-ready@v0
|
|
122
|
+
with:
|
|
123
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
124
|
+
rules: .agent-ready/rules.yaml # optional
|
|
125
|
+
comment-on-issue: true
|
|
126
|
+
fail-on-not-ready: true
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The action fetches the triggering issue from the GitHub API, normalizes it into the linter's ticket shape, runs the lint, posts the result as a comment, and writes outputs (`ready`, `failed-count`, `warnings-count`). When `fail-on-not-ready: true`, the step exits non-zero so the issue check shows red until the ticket is fixed.
|
|
130
|
+
|
|
131
|
+
> **Planned for v0.1:** the action will also set/remove an `agent-ready` label on the issue so downstream agent workflows can listen for the label rather than parsing comment text.
|
|
132
|
+
|
|
133
|
+
## Rule pack format
|
|
134
|
+
|
|
135
|
+
Rule packs are plain YAML. Mix built-ins with custom rules of `type: regex`:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
# .agent-ready/rules.yaml
|
|
139
|
+
version: 1
|
|
140
|
+
extends: default
|
|
141
|
+
|
|
142
|
+
rules:
|
|
143
|
+
has-acceptance-criteria:
|
|
144
|
+
enabled: true
|
|
145
|
+
min_count: 2
|
|
146
|
+
severity: error
|
|
147
|
+
|
|
148
|
+
no-ambiguous-verbs:
|
|
149
|
+
enabled: true
|
|
150
|
+
severity: warn
|
|
151
|
+
extra_terms: [tidy, polish, modernize]
|
|
152
|
+
|
|
153
|
+
custom-mentions-jira-epic:
|
|
154
|
+
type: regex
|
|
155
|
+
pattern: 'EPIC-\d+'
|
|
156
|
+
field: body
|
|
157
|
+
severity: error
|
|
158
|
+
message: "Ticket must link to a parent epic (EPIC-XXX)"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
JSON Schemas are published in [`schema/`](schema/) — both the rule pack format ([`rule-pack.schema.json`](schema/rule-pack.schema.json)) and the CLI output ([`output.schema.json`](schema/output.schema.json)). The output schema is stable across versions; downstream tools (e.g. [Gatepack](#how-it-composes-with-the-rest-of-your-stack)) can safely consume it.
|
|
162
|
+
|
|
163
|
+
## How it composes with the rest of your stack
|
|
164
|
+
|
|
165
|
+
`agent-ready` is the **first** gate. It doesn't replace your existing tools — it makes them work better.
|
|
166
|
+
|
|
167
|
+
| Tool | What it does | When it runs |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| **`agent-ready`** | Is this *ticket* ready for an agent? | Issue open → before agent picks up |
|
|
170
|
+
| Spec Kit / Linear specs | Authoring help for the spec itself | While writing the ticket |
|
|
171
|
+
| Your AI coding agent | Implements the change | After `agent-ready` passes |
|
|
172
|
+
| Gatepack *(planned)* | Per-PR signed evidence bundle (includes `agent-ready` pre-flight result) | After agent submits PR |
|
|
173
|
+
| Evidence Gate Action | Traditional CI evidence (SBOM, SAST, tests) | During CI |
|
|
174
|
+
| OPA / your policy engine | Decision enforcement | Throughout |
|
|
175
|
+
|
|
176
|
+
## Status
|
|
177
|
+
|
|
178
|
+
**v0.0.2.** Schemas, CLI, file and GitHub adapters, 10 built-in rules, regex custom rules, JSON/markdown/text renderers, GitHub Action (Docker-based), and a CI workflow that runs the bad/good demo on every PR. All verified end-to-end.
|
|
179
|
+
|
|
180
|
+
## Releases
|
|
181
|
+
|
|
182
|
+
GitHub Action users should pin either:
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
- uses: Schoaib/agent-ready@v0.0.2 # exact release
|
|
186
|
+
- uses: Schoaib/agent-ready@v0 # latest v0 release
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The Marketplace listing is published from GitHub Releases. For each release, verify CI, create the version tag, publish the release, and select **Publish this Action to the GitHub Marketplace**.
|
|
190
|
+
|
|
191
|
+
### Roadmap
|
|
192
|
+
|
|
193
|
+
**v0.1 — honest the rest of the way**
|
|
194
|
+
- Native CLI adapters for Jira and Linear
|
|
195
|
+
- `links-resolve` rule
|
|
196
|
+
- `restricted-paths-declared` rule (links to OPA's `restricted-paths.rego`)
|
|
197
|
+
- `path_recommendation` (A/B/C), `context_tier` (T1/T2/T3), and `risk_classification` as first-class output fields — driven by a rule pack
|
|
198
|
+
- `agent-ready` label setter on the issue (so agent workflows can listen for the label)
|
|
199
|
+
- SARIF output format
|
|
200
|
+
- Output fields for Gatepack ingestion: rule pack version + hash, source URL, adapter metadata
|
|
201
|
+
- LLM judge for `no-ambiguous-verbs` (opt-in)
|
|
202
|
+
|
|
203
|
+
**v0.2**
|
|
204
|
+
- VS Code extension: lint as you type the issue
|
|
205
|
+
- Node plugin loader for custom rules (beyond regex)
|
|
206
|
+
|
|
207
|
+
**v0.3**
|
|
208
|
+
- Companion product `gatepack` — signed per-PR evidence bundle that includes the `agent-ready` pre-flight result as one of its input sources
|
|
209
|
+
|
|
210
|
+
## Contributing
|
|
211
|
+
|
|
212
|
+
Rules are the easiest contribution path. One rule = one entry in `src/rules/built-in.ts` + one demonstration in `examples/tickets/`. PRs welcome.
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export async function loadTicketFromFile(path) {
|
|
3
|
+
const raw = await readFile(path, "utf8");
|
|
4
|
+
const obj = JSON.parse(raw);
|
|
5
|
+
if (!obj.id || !obj.title) {
|
|
6
|
+
throw new Error(`Invalid ticket file ${path}: missing 'id' or 'title'`);
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
id: String(obj.id),
|
|
10
|
+
title: String(obj.title),
|
|
11
|
+
body: String(obj.body ?? ""),
|
|
12
|
+
labels: Array.isArray(obj.labels) ? obj.labels.map(String) : [],
|
|
13
|
+
url: obj.url
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
function parseGitHubTarget(target) {
|
|
3
|
+
const shorthand = target.match(/^([^/\s#]+)\/([^/\s#]+)#(\d+)$/);
|
|
4
|
+
if (shorthand) {
|
|
5
|
+
return {
|
|
6
|
+
owner: shorthand[1],
|
|
7
|
+
repo: shorthand[2],
|
|
8
|
+
issueNumber: Number(shorthand[3])
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const url = target.match(/^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/issues\/(\d+)(?:[/?#].*)?$/);
|
|
12
|
+
if (url) {
|
|
13
|
+
return {
|
|
14
|
+
owner: url[1],
|
|
15
|
+
repo: url[2],
|
|
16
|
+
issueNumber: Number(url[3])
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
throw new Error("Invalid GitHub target. Use owner/repo#123 or https://github.com/owner/repo/issues/123");
|
|
20
|
+
}
|
|
21
|
+
function githubToken() {
|
|
22
|
+
const envToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
23
|
+
if (envToken)
|
|
24
|
+
return envToken;
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync("gh", ["auth", "token"], {
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
29
|
+
}).trim();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function normalizeLabels(labels) {
|
|
36
|
+
return labels
|
|
37
|
+
.map((label) => {
|
|
38
|
+
if (typeof label === "string")
|
|
39
|
+
return label;
|
|
40
|
+
return label.name ?? "";
|
|
41
|
+
})
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
export async function loadTicketFromGitHub(target) {
|
|
45
|
+
const { owner, repo, issueNumber } = parseGitHubTarget(target);
|
|
46
|
+
const token = githubToken();
|
|
47
|
+
const headers = {
|
|
48
|
+
Accept: "application/vnd.github+json",
|
|
49
|
+
"User-Agent": "agent-ready"
|
|
50
|
+
};
|
|
51
|
+
if (token) {
|
|
52
|
+
headers.Authorization = `Bearer ${token}`;
|
|
53
|
+
}
|
|
54
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`, { headers });
|
|
55
|
+
const issue = (await res.json());
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const detail = issue.message ? `: ${issue.message}` : "";
|
|
58
|
+
throw new Error(`GitHub issue fetch failed (${res.status})${detail}`);
|
|
59
|
+
}
|
|
60
|
+
if (!issue.title) {
|
|
61
|
+
throw new Error(`GitHub issue ${owner}/${repo}#${issueNumber} has no title`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
id: `#${issue.number}`,
|
|
65
|
+
title: issue.title,
|
|
66
|
+
body: issue.body ?? "",
|
|
67
|
+
labels: normalizeLabels(issue.labels),
|
|
68
|
+
url: issue.html_url
|
|
69
|
+
};
|
|
70
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { parse as parseYaml } from "yaml";
|
|
6
|
+
import { lintTicket } from "./lint.js";
|
|
7
|
+
import { loadTicketFromFile } from "./adapters/file.js";
|
|
8
|
+
import { loadTicketFromGitHub } from "./adapters/github.js";
|
|
9
|
+
import { renderMarkdown, renderText } from "./render/markdown.js";
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const args = { command: "help", adapter: "file", format: "text" };
|
|
14
|
+
const rest = [];
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const a = argv[i];
|
|
17
|
+
if (a === "--adapter")
|
|
18
|
+
args.adapter = argv[++i];
|
|
19
|
+
else if (a === "--rules")
|
|
20
|
+
args.rules = argv[++i];
|
|
21
|
+
else if (a === "--format")
|
|
22
|
+
args.format = argv[++i];
|
|
23
|
+
else if (a === "-h" || a === "--help")
|
|
24
|
+
args.command = "help";
|
|
25
|
+
else if (a === "-v" || a === "--version")
|
|
26
|
+
args.command = "version";
|
|
27
|
+
else
|
|
28
|
+
rest.push(a);
|
|
29
|
+
}
|
|
30
|
+
if (rest[0] === "check" && rest[1]) {
|
|
31
|
+
args.command = "check";
|
|
32
|
+
args.target = rest[1];
|
|
33
|
+
}
|
|
34
|
+
else if (rest[0] === "help") {
|
|
35
|
+
args.command = "help";
|
|
36
|
+
}
|
|
37
|
+
else if (rest[0]) {
|
|
38
|
+
args.command = "check";
|
|
39
|
+
args.target = rest[0];
|
|
40
|
+
}
|
|
41
|
+
return args;
|
|
42
|
+
}
|
|
43
|
+
async function loadRulePack(path) {
|
|
44
|
+
const defaultPath = resolve(__dirname, "..", "rule-packs", "default.yaml");
|
|
45
|
+
const file = path ?? defaultPath;
|
|
46
|
+
const raw = await readFile(file, "utf8");
|
|
47
|
+
const pack = parseYaml(raw);
|
|
48
|
+
return { pack, name: path ? file : "default" };
|
|
49
|
+
}
|
|
50
|
+
function usage() {
|
|
51
|
+
return `agent-ready — Make every ticket ready for AI coding agents.
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
agent-ready check <ticket-or-file> [--adapter file|github|jira|linear] [--rules <path>] [--format text|markdown|json]
|
|
55
|
+
agent-ready --version
|
|
56
|
+
agent-ready --help
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
agent-ready check examples/tickets/bad-ticket.json
|
|
60
|
+
agent-ready check examples/tickets/good-ticket.json --format markdown
|
|
61
|
+
agent-ready check owner/repo#123 --adapter github
|
|
62
|
+
agent-ready check https://github.com/owner/repo/issues/123 --adapter github
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
async function main() {
|
|
66
|
+
const args = parseArgs(process.argv.slice(2));
|
|
67
|
+
if (args.command === "version") {
|
|
68
|
+
console.log("0.0.1");
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (args.command === "help" || !args.target) {
|
|
72
|
+
console.log(usage());
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
if (args.adapter === "jira" || args.adapter === "linear") {
|
|
76
|
+
console.error(`Adapter '${args.adapter}' is not implemented yet. Use --adapter file or --adapter github.`);
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
const ticket = args.adapter === "github"
|
|
80
|
+
? await loadTicketFromGitHub(args.target)
|
|
81
|
+
: await loadTicketFromFile(args.target);
|
|
82
|
+
const { pack, name } = await loadRulePack(args.rules);
|
|
83
|
+
const out = lintTicket(ticket, pack, { adapter: args.adapter, rulePackName: name });
|
|
84
|
+
if (args.format === "json")
|
|
85
|
+
console.log(JSON.stringify(out, null, 2));
|
|
86
|
+
else if (args.format === "markdown")
|
|
87
|
+
console.log(renderMarkdown(out));
|
|
88
|
+
else
|
|
89
|
+
console.log(renderText(out));
|
|
90
|
+
return out.ready ? 0 : 1;
|
|
91
|
+
}
|
|
92
|
+
main()
|
|
93
|
+
.then((code) => process.exit(code))
|
|
94
|
+
.catch((err) => {
|
|
95
|
+
console.error(`agent-ready: ${err?.message ?? err}`);
|
|
96
|
+
process.exit(2);
|
|
97
|
+
});
|
package/dist/lint.d.ts
ADDED
package/dist/lint.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BUILTIN_RULES, runCustomRegex } from "./rules/built-in.js";
|
|
2
|
+
const VERSION = "0.0.1";
|
|
3
|
+
export function lintTicket(ticket, pack, opts) {
|
|
4
|
+
const checks = [];
|
|
5
|
+
const ruleConfigs = pack.rules || {};
|
|
6
|
+
const builtinIds = new Set(BUILTIN_RULES.map((r) => r.id));
|
|
7
|
+
for (const rule of BUILTIN_RULES) {
|
|
8
|
+
const cfg = ruleConfigs[rule.id] ?? { enabled: true };
|
|
9
|
+
if (cfg.enabled === false)
|
|
10
|
+
continue;
|
|
11
|
+
checks.push(rule.run(ticket, cfg));
|
|
12
|
+
}
|
|
13
|
+
for (const [id, cfg] of Object.entries(ruleConfigs)) {
|
|
14
|
+
if (builtinIds.has(id))
|
|
15
|
+
continue;
|
|
16
|
+
if (cfg.enabled === false)
|
|
17
|
+
continue;
|
|
18
|
+
if (cfg.type === "regex") {
|
|
19
|
+
checks.push(runCustomRegex(ticket, id, cfg));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const failed = checks.filter((c) => c.status === "fail" && c.severity === "error").length;
|
|
23
|
+
const warnings = checks.filter((c) => c.status === "fail" && c.severity === "warn").length;
|
|
24
|
+
const passed = checks.filter((c) => c.status === "pass").length;
|
|
25
|
+
return {
|
|
26
|
+
schema_version: "1.0",
|
|
27
|
+
agent_ready_version: VERSION,
|
|
28
|
+
ticket_id: ticket.id,
|
|
29
|
+
adapter: opts.adapter,
|
|
30
|
+
rule_pack: opts.rulePackName,
|
|
31
|
+
checked_at: new Date().toISOString(),
|
|
32
|
+
ready: failed === 0,
|
|
33
|
+
summary: { passed, failed, warnings },
|
|
34
|
+
checks
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function renderMarkdown(out) {
|
|
2
|
+
const head = out.ready
|
|
3
|
+
? `✓ **${out.ticket_id}** — ready for an agent (${out.summary.passed} checks passed${out.summary.warnings ? `, ${out.summary.warnings} warning(s)` : ""})`
|
|
4
|
+
: `✗ **${out.ticket_id}** — not ready (${out.summary.failed} blocker(s), ${out.summary.warnings} warning(s))`;
|
|
5
|
+
const lines = [`### agent-ready check`, "", head, ""];
|
|
6
|
+
if (out.checks.length) {
|
|
7
|
+
lines.push("| | Rule | Status |");
|
|
8
|
+
lines.push("|---|---|---|");
|
|
9
|
+
for (const c of out.checks) {
|
|
10
|
+
const icon = c.status === "pass" ? "✓" : c.severity === "warn" ? "⚠" : c.status === "skip" ? "—" : "✗";
|
|
11
|
+
lines.push(`| ${icon} | \`${c.id}\` | ${c.message}${c.hint ? ` — _${c.hint}_` : ""} |`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (!out.ready) {
|
|
15
|
+
lines.push("", "**Fix the blockers above before handing this ticket to an AI agent.**");
|
|
16
|
+
}
|
|
17
|
+
return lines.join("\n");
|
|
18
|
+
}
|
|
19
|
+
export function renderText(out) {
|
|
20
|
+
const head = out.ready
|
|
21
|
+
? `✓ ${out.ticket_id} ready (${out.summary.passed} checks passed${out.summary.warnings ? `, ${out.summary.warnings} warning(s)` : ""})`
|
|
22
|
+
: `✗ ${out.ticket_id} not ready (${out.summary.failed} blocker(s), ${out.summary.warnings} warning(s))`;
|
|
23
|
+
const rows = out.checks.map((c) => {
|
|
24
|
+
const icon = c.status === "pass" ? "✓" : c.severity === "warn" ? "⚠" : c.status === "skip" ? "·" : "✗";
|
|
25
|
+
return ` ${icon} ${c.id.padEnd(28)} ${c.message}`;
|
|
26
|
+
});
|
|
27
|
+
return [head, "", ...rows].join("\n");
|
|
28
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const AMBIGUOUS_VERBS = [
|
|
2
|
+
"improve", "optimize", "clean up", "cleanup", "refactor",
|
|
3
|
+
"enhance", "fix it", "make it better", "tidy", "polish", "modernize"
|
|
4
|
+
];
|
|
5
|
+
const TRIBAL_PHRASES = [
|
|
6
|
+
"as discussed", "you know what i mean", "the usual way",
|
|
7
|
+
"like before", "as agreed", "per our chat", "as we talked about"
|
|
8
|
+
];
|
|
9
|
+
const RISK_LABELS = ["risk:low", "risk:medium", "risk:high"];
|
|
10
|
+
const SIZE_LABELS = ["size:s", "size:m", "size:l", "size:xs", "size:xl"];
|
|
11
|
+
function pass(id, severity, message) {
|
|
12
|
+
return { id, severity, status: "pass", message };
|
|
13
|
+
}
|
|
14
|
+
function fail(id, severity, message, hint) {
|
|
15
|
+
return { id, severity, status: "fail", message, hint };
|
|
16
|
+
}
|
|
17
|
+
function severityOf(cfg, fallback) {
|
|
18
|
+
return cfg.severity ?? fallback;
|
|
19
|
+
}
|
|
20
|
+
function bodyLower(t) {
|
|
21
|
+
return (t.body || "").toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
function labelsLower(t) {
|
|
24
|
+
return (t.labels || []).map((l) => l.toLowerCase());
|
|
25
|
+
}
|
|
26
|
+
const hasAcceptanceCriteria = {
|
|
27
|
+
id: "has-acceptance-criteria",
|
|
28
|
+
defaultSeverity: "error",
|
|
29
|
+
run(ticket, cfg) {
|
|
30
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
31
|
+
const min = cfg.min_count ?? 1;
|
|
32
|
+
const body = ticket.body || "";
|
|
33
|
+
const matches = body.match(/^\s*[-*]\s*\[[ x]\]\s+.+$/gm) || [];
|
|
34
|
+
const numbered = body.match(/^\s*\d+\.\s+.+$/gm) || [];
|
|
35
|
+
const givenWhenThen = body.match(/given\s+.+\bwhen\b/gi) || [];
|
|
36
|
+
const total = matches.length + numbered.length + givenWhenThen.length;
|
|
37
|
+
const headingPresent = /acceptance\s+criteria/i.test(body);
|
|
38
|
+
if (total >= min || (headingPresent && total > 0)) {
|
|
39
|
+
return pass(this.id, sev, `Found ${total} acceptance criteria`);
|
|
40
|
+
}
|
|
41
|
+
return fail(this.id, sev, `No acceptance criteria found (need at least ${min})`, "Add a checklist, numbered list, or Given/When/Then under an 'Acceptance criteria' heading.");
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const hasDefinitionOfDone = {
|
|
45
|
+
id: "has-definition-of-done",
|
|
46
|
+
defaultSeverity: "warn",
|
|
47
|
+
run(ticket, cfg) {
|
|
48
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
49
|
+
const found = /definition\s+of\s+done|\bdod\b/i.test(ticket.body || "");
|
|
50
|
+
return found
|
|
51
|
+
? pass(this.id, sev, "DoD section found")
|
|
52
|
+
: fail(this.id, sev, "No Definition of Done found", "Add a 'Definition of Done' section listing test, doc, and review requirements.");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const hasRepoTarget = {
|
|
56
|
+
id: "has-repo-target",
|
|
57
|
+
defaultSeverity: "error",
|
|
58
|
+
run(ticket, cfg) {
|
|
59
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
60
|
+
const body = ticket.body || "";
|
|
61
|
+
const inBody = /(^|\n)\s*repo\s*:\s*\S+/i.test(body);
|
|
62
|
+
const inLabel = labelsLower(ticket).some((l) => l.startsWith("repo:"));
|
|
63
|
+
if (inBody || inLabel)
|
|
64
|
+
return pass(this.id, sev, "Target repo specified");
|
|
65
|
+
return fail(this.id, sev, "Ticket does not specify the target repo", "Add `repo: owner/name` to the body or a `repo:<name>` label.");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const hasRiskClassification = {
|
|
69
|
+
id: "has-risk-classification",
|
|
70
|
+
defaultSeverity: "error",
|
|
71
|
+
run(ticket, cfg) {
|
|
72
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
73
|
+
const found = labelsLower(ticket).some((l) => RISK_LABELS.includes(l));
|
|
74
|
+
return found
|
|
75
|
+
? pass(this.id, sev, "Risk classification label found")
|
|
76
|
+
: fail(this.id, sev, "No risk classification label", "Add one of: risk:low, risk:medium, risk:high");
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const hasTestExpectations = {
|
|
80
|
+
id: "has-test-expectations",
|
|
81
|
+
defaultSeverity: "warn",
|
|
82
|
+
run(ticket, cfg) {
|
|
83
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
84
|
+
const body = bodyLower(ticket);
|
|
85
|
+
const found = /how to verify|test plan|playwright|jest|pytest|unit test|e2e/i.test(body);
|
|
86
|
+
return found
|
|
87
|
+
? pass(this.id, sev, "Test expectations described")
|
|
88
|
+
: fail(this.id, sev, "No test expectations described", "Add a 'How to verify' or 'Test plan' section.");
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const noAmbiguousVerbs = {
|
|
92
|
+
id: "no-ambiguous-verbs",
|
|
93
|
+
defaultSeverity: "warn",
|
|
94
|
+
run(ticket, cfg) {
|
|
95
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
96
|
+
const extras = (cfg.extra_terms || []).map((s) => s.toLowerCase());
|
|
97
|
+
const all = [...AMBIGUOUS_VERBS, ...extras];
|
|
98
|
+
const haystack = `${ticket.title} ${ticket.body}`.toLowerCase();
|
|
99
|
+
const hits = all.filter((v) => new RegExp(`\\b${v}\\b`, "i").test(haystack));
|
|
100
|
+
return hits.length === 0
|
|
101
|
+
? pass(this.id, sev, "No ambiguous verbs")
|
|
102
|
+
: fail(this.id, sev, `Ambiguous verb(s): ${hits.join(", ")}`, "Prefer concrete verbs like 'add', 'fix', 'remove', 'replace'.");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const bodyMinLength = {
|
|
106
|
+
id: "body-min-length",
|
|
107
|
+
defaultSeverity: "error",
|
|
108
|
+
run(ticket, cfg) {
|
|
109
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
110
|
+
const min = cfg.min_count ?? 100;
|
|
111
|
+
const len = (ticket.body || "").trim().length;
|
|
112
|
+
return len >= min
|
|
113
|
+
? pass(this.id, sev, `Body length ${len} >= ${min}`)
|
|
114
|
+
: fail(this.id, sev, `Body too short: ${len} chars (need >= ${min})`, "Expand the description with context, AC, and test plan.");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const noTribalKnowledge = {
|
|
118
|
+
id: "no-tribal-knowledge",
|
|
119
|
+
defaultSeverity: "warn",
|
|
120
|
+
run(ticket, cfg) {
|
|
121
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
122
|
+
const body = bodyLower(ticket);
|
|
123
|
+
const hits = TRIBAL_PHRASES.filter((p) => body.includes(p));
|
|
124
|
+
return hits.length === 0
|
|
125
|
+
? pass(this.id, sev, "No tribal-knowledge phrases")
|
|
126
|
+
: fail(this.id, sev, `Tribal-knowledge phrase(s): ${hits.join("; ")}`, "Replace with explicit detail. The agent has no prior chat context.");
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const tShirtSizePresent = {
|
|
130
|
+
id: "t-shirt-size-present",
|
|
131
|
+
defaultSeverity: "warn",
|
|
132
|
+
run(ticket, cfg) {
|
|
133
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
134
|
+
const body = ticket.body || "";
|
|
135
|
+
const inBody = /(^|\n)\s*size\s*:\s*(xs|s|m|l|xl)\b/i.test(body);
|
|
136
|
+
const inLabel = labelsLower(ticket).some((l) => SIZE_LABELS.includes(l));
|
|
137
|
+
if (inBody || inLabel)
|
|
138
|
+
return pass(this.id, sev, "T-shirt size present");
|
|
139
|
+
return fail(this.id, sev, "No t-shirt size estimate", "Add `size: S|M|L|XL` to the body or a `size:<x>` label.");
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const hasDesignLink = {
|
|
143
|
+
id: "has-design-link",
|
|
144
|
+
defaultSeverity: "warn",
|
|
145
|
+
run(ticket, cfg) {
|
|
146
|
+
const sev = severityOf(cfg, this.defaultSeverity);
|
|
147
|
+
const triggerLabels = (cfg.labels || ["ui", "ux", "frontend"]).map((s) => s.toLowerCase());
|
|
148
|
+
const ticketLabels = labelsLower(ticket);
|
|
149
|
+
const triggered = triggerLabels.some((l) => ticketLabels.includes(l));
|
|
150
|
+
if (!triggered)
|
|
151
|
+
return { id: this.id, severity: sev, status: "skip", message: "Not a UI ticket" };
|
|
152
|
+
const body = ticket.body || "";
|
|
153
|
+
const found = /figma\.com|ardoq\.com|miro\.com|excalidraw\.com/i.test(body);
|
|
154
|
+
return found
|
|
155
|
+
? pass(this.id, sev, "Design link found")
|
|
156
|
+
: fail(this.id, sev, "UI ticket has no design link", "Link to the Figma/Ardoq/Miro source of truth.");
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
export const BUILTIN_RULES = [
|
|
160
|
+
hasAcceptanceCriteria,
|
|
161
|
+
hasDefinitionOfDone,
|
|
162
|
+
hasRepoTarget,
|
|
163
|
+
hasRiskClassification,
|
|
164
|
+
hasTestExpectations,
|
|
165
|
+
noAmbiguousVerbs,
|
|
166
|
+
bodyMinLength,
|
|
167
|
+
noTribalKnowledge,
|
|
168
|
+
tShirtSizePresent,
|
|
169
|
+
hasDesignLink
|
|
170
|
+
];
|
|
171
|
+
export function runCustomRegex(ticket, id, cfg) {
|
|
172
|
+
const sev = cfg.severity ?? "error";
|
|
173
|
+
if (!cfg.pattern || !cfg.field) {
|
|
174
|
+
return fail(id, sev, "Invalid custom rule (missing pattern or field)");
|
|
175
|
+
}
|
|
176
|
+
const re = new RegExp(cfg.pattern, cfg.flags ?? "i");
|
|
177
|
+
let haystack = "";
|
|
178
|
+
switch (cfg.field) {
|
|
179
|
+
case "title":
|
|
180
|
+
haystack = ticket.title || "";
|
|
181
|
+
break;
|
|
182
|
+
case "body":
|
|
183
|
+
haystack = ticket.body || "";
|
|
184
|
+
break;
|
|
185
|
+
case "labels":
|
|
186
|
+
haystack = (ticket.labels || []).join(" ");
|
|
187
|
+
break;
|
|
188
|
+
case "any":
|
|
189
|
+
haystack = `${ticket.title || ""} ${ticket.body || ""} ${(ticket.labels || []).join(" ")}`;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
const matched = re.test(haystack);
|
|
193
|
+
const want = cfg.must_match !== false;
|
|
194
|
+
const ok = matched === want;
|
|
195
|
+
return ok
|
|
196
|
+
? pass(id, sev, cfg.message || `Custom regex passed (${cfg.field})`)
|
|
197
|
+
: fail(id, sev, cfg.message || `Custom regex failed (${cfg.field})`);
|
|
198
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type Severity = "error" | "warn" | "info";
|
|
2
|
+
export interface Ticket {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
body: string;
|
|
6
|
+
labels: string[];
|
|
7
|
+
url?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RuleConfig {
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
severity?: Severity;
|
|
12
|
+
min_count?: number;
|
|
13
|
+
extra_terms?: string[];
|
|
14
|
+
labels?: string[];
|
|
15
|
+
type?: "regex";
|
|
16
|
+
pattern?: string;
|
|
17
|
+
flags?: string;
|
|
18
|
+
field?: "title" | "body" | "labels" | "any";
|
|
19
|
+
must_match?: boolean;
|
|
20
|
+
message?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RulePack {
|
|
23
|
+
version: 1;
|
|
24
|
+
extends?: string;
|
|
25
|
+
rules: Record<string, RuleConfig>;
|
|
26
|
+
}
|
|
27
|
+
export interface CheckResult {
|
|
28
|
+
id: string;
|
|
29
|
+
severity: Severity;
|
|
30
|
+
status: "pass" | "fail" | "skip";
|
|
31
|
+
message: string;
|
|
32
|
+
hint?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface LintOutput {
|
|
35
|
+
schema_version: "1.0";
|
|
36
|
+
agent_ready_version: string;
|
|
37
|
+
ticket_id: string;
|
|
38
|
+
adapter: string;
|
|
39
|
+
rule_pack: string;
|
|
40
|
+
checked_at: string;
|
|
41
|
+
ready: boolean;
|
|
42
|
+
summary: {
|
|
43
|
+
passed: number;
|
|
44
|
+
failed: number;
|
|
45
|
+
warnings: number;
|
|
46
|
+
};
|
|
47
|
+
checks: CheckResult[];
|
|
48
|
+
}
|
|
49
|
+
export interface Rule {
|
|
50
|
+
id: string;
|
|
51
|
+
defaultSeverity: Severity;
|
|
52
|
+
run(ticket: Ticket, config: RuleConfig): CheckResult;
|
|
53
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syedshoaib/agent-ready",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Make every ticket ready for AI coding agents — a Definition-of-Ready linter.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-ready": "dist/cli.js",
|
|
8
|
+
"story-lint": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/lint.js",
|
|
11
|
+
"types": "dist/lint.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"rule-packs",
|
|
15
|
+
"schema",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"start": "node dist/cli.js",
|
|
22
|
+
"check:bad": "node dist/cli.js check examples/tickets/bad-ticket.json",
|
|
23
|
+
"check:good": "node dist/cli.js check examples/tickets/good-ticket.json",
|
|
24
|
+
"test": "node --test test/"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"ai",
|
|
31
|
+
"agents",
|
|
32
|
+
"sdlc",
|
|
33
|
+
"linter",
|
|
34
|
+
"definition-of-ready",
|
|
35
|
+
"jira",
|
|
36
|
+
"github-issues",
|
|
37
|
+
"copilot",
|
|
38
|
+
"claude-code",
|
|
39
|
+
"cursor"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"yaml": "^2.6.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.9.0",
|
|
47
|
+
"typescript": "^5.6.0"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/Schoaib/agent-ready.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/Schoaib/agent-ready/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/Schoaib/agent-ready#readme"
|
|
57
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# agent-ready default rule pack
|
|
2
|
+
# Loaded automatically when no --rules flag is passed.
|
|
3
|
+
# Override per-rule by writing your own .agent-ready/rules.yaml with `extends: default`.
|
|
4
|
+
|
|
5
|
+
version: 1
|
|
6
|
+
|
|
7
|
+
rules:
|
|
8
|
+
has-acceptance-criteria:
|
|
9
|
+
enabled: true
|
|
10
|
+
min_count: 1
|
|
11
|
+
severity: error
|
|
12
|
+
|
|
13
|
+
has-definition-of-done:
|
|
14
|
+
enabled: true
|
|
15
|
+
severity: warn
|
|
16
|
+
|
|
17
|
+
has-repo-target:
|
|
18
|
+
enabled: true
|
|
19
|
+
severity: error
|
|
20
|
+
|
|
21
|
+
has-risk-classification:
|
|
22
|
+
enabled: true
|
|
23
|
+
severity: error
|
|
24
|
+
|
|
25
|
+
has-test-expectations:
|
|
26
|
+
enabled: true
|
|
27
|
+
severity: warn
|
|
28
|
+
|
|
29
|
+
no-ambiguous-verbs:
|
|
30
|
+
enabled: true
|
|
31
|
+
severity: warn
|
|
32
|
+
extra_terms: []
|
|
33
|
+
|
|
34
|
+
body-min-length:
|
|
35
|
+
enabled: true
|
|
36
|
+
min_count: 100
|
|
37
|
+
severity: error
|
|
38
|
+
|
|
39
|
+
no-tribal-knowledge:
|
|
40
|
+
enabled: true
|
|
41
|
+
severity: warn
|
|
42
|
+
|
|
43
|
+
t-shirt-size-present:
|
|
44
|
+
enabled: true
|
|
45
|
+
severity: warn
|
|
46
|
+
|
|
47
|
+
has-design-link:
|
|
48
|
+
enabled: true
|
|
49
|
+
severity: warn
|
|
50
|
+
labels: [ui, ux, frontend]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://agent-ready.dev/schema/output.v1.json",
|
|
4
|
+
"title": "agent-ready CLI output",
|
|
5
|
+
"description": "Machine-readable result of an agent-ready check. Stable across versions; safe to consume in CI and downstream tools (e.g. Gatepack).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema_version", "ticket_id", "ready", "checks", "summary"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "type": "string", "const": "1.0" },
|
|
10
|
+
"agent_ready_version": { "type": "string" },
|
|
11
|
+
"ticket_id": { "type": "string" },
|
|
12
|
+
"adapter": { "type": "string", "enum": ["file", "github", "jira", "linear"] },
|
|
13
|
+
"rule_pack": { "type": "string" },
|
|
14
|
+
"checked_at": { "type": "string", "format": "date-time" },
|
|
15
|
+
"ready": {
|
|
16
|
+
"type": "boolean",
|
|
17
|
+
"description": "True iff zero error-severity rules failed."
|
|
18
|
+
},
|
|
19
|
+
"summary": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["passed", "failed", "warnings"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"passed": { "type": "integer", "minimum": 0 },
|
|
24
|
+
"failed": { "type": "integer", "minimum": 0 },
|
|
25
|
+
"warnings": { "type": "integer", "minimum": 0 }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"checks": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"required": ["id", "severity", "status", "message"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"id": { "type": "string" },
|
|
35
|
+
"severity": { "type": "string", "enum": ["error", "warn", "info"] },
|
|
36
|
+
"status": { "type": "string", "enum": ["pass", "fail", "skip"] },
|
|
37
|
+
"message": { "type": "string" },
|
|
38
|
+
"hint": { "type": "string" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://agent-ready.dev/schema/rule-pack.v1.json",
|
|
4
|
+
"title": "agent-ready rule pack",
|
|
5
|
+
"description": "Declarative ruleset describing what makes a ticket 'ready' for an AI coding agent.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"const": 1,
|
|
12
|
+
"description": "Rule pack schema version."
|
|
13
|
+
},
|
|
14
|
+
"extends": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Name of a built-in pack to inherit from (e.g. 'default', 'strict'). Local overrides win."
|
|
17
|
+
},
|
|
18
|
+
"rules": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"description": "Map of rule-id -> rule configuration. Rule ids match built-in rule names, or define a 'type' for custom rules.",
|
|
21
|
+
"additionalProperties": {
|
|
22
|
+
"oneOf": [
|
|
23
|
+
{ "$ref": "#/$defs/builtinRuleConfig" },
|
|
24
|
+
{ "$ref": "#/$defs/customRegexRule" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"$defs": {
|
|
30
|
+
"severity": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"enum": ["error", "warn", "info"],
|
|
33
|
+
"default": "error"
|
|
34
|
+
},
|
|
35
|
+
"builtinRuleConfig": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"enabled": { "type": "boolean", "default": true },
|
|
39
|
+
"severity": { "$ref": "#/$defs/severity" },
|
|
40
|
+
"min_count": { "type": "integer", "minimum": 1 },
|
|
41
|
+
"extra_terms": { "type": "array", "items": { "type": "string" } },
|
|
42
|
+
"labels": { "type": "array", "items": { "type": "string" } }
|
|
43
|
+
},
|
|
44
|
+
"additionalProperties": true
|
|
45
|
+
},
|
|
46
|
+
"customRegexRule": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": ["type", "pattern", "field"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"type": { "const": "regex" },
|
|
51
|
+
"pattern": { "type": "string", "description": "JavaScript regex pattern." },
|
|
52
|
+
"flags": { "type": "string", "default": "i" },
|
|
53
|
+
"field": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"enum": ["title", "body", "labels", "any"],
|
|
56
|
+
"description": "Where to apply the pattern."
|
|
57
|
+
},
|
|
58
|
+
"must_match": { "type": "boolean", "default": true },
|
|
59
|
+
"severity": { "$ref": "#/$defs/severity" },
|
|
60
|
+
"message": { "type": "string" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|