@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 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,2 @@
1
+ import type { Ticket } from "../types.js";
2
+ export declare function loadTicketFromFile(path: string): Promise<Ticket>;
@@ -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,2 @@
1
+ import type { Ticket } from "../types.js";
2
+ export declare function loadTicketFromGitHub(target: string): Promise<Ticket>;
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
@@ -0,0 +1,5 @@
1
+ import type { LintOutput, RulePack, Ticket } from "./types.js";
2
+ export declare function lintTicket(ticket: Ticket, pack: RulePack, opts: {
3
+ adapter: string;
4
+ rulePackName: string;
5
+ }): LintOutput;
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,3 @@
1
+ import type { LintOutput } from "../types.js";
2
+ export declare function renderMarkdown(out: LintOutput): string;
3
+ export declare function renderText(out: LintOutput): string;
@@ -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,3 @@
1
+ import type { Rule, Ticket, RuleConfig, CheckResult } from "../types.js";
2
+ export declare const BUILTIN_RULES: Rule[];
3
+ export declare function runCustomRegex(ticket: Ticket, id: string, cfg: RuleConfig): CheckResult;
@@ -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
+ }
@@ -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
+ }