kataster 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/acceptance-tester.md +53 -0
- package/.claude/agents/code-reviewer.md +56 -0
- package/.claude/agents/domain-architect.md +40 -0
- package/.claude/agents/lead-dev.md +50 -0
- package/.claude/agents/product-owner.md +39 -0
- package/.claude/agents/repo-analyst.md +78 -0
- package/.claude/agents/requirements-engineer.md +44 -0
- package/.claude/agents/ux-designer.md +64 -0
- package/.claude/hooks/bash-guard.ts +92 -0
- package/.claude/hooks/code-check.ts +37 -0
- package/.claude/hooks/path-guard.ts +34 -0
- package/.claude/hooks/path-rules.ts +66 -0
- package/.claude/hooks/project-check.ts +108 -0
- package/.claude/hooks/protect-files.ts +55 -0
- package/.claude/hooks/session-start.ts +213 -0
- package/.claude/hooks/stop-gate.ts +56 -0
- package/.claude/hooks/validate-docs.ts +58 -0
- package/.claude/lib/architecture.ts +736 -0
- package/.claude/lib/backlog-github.ts +454 -0
- package/.claude/lib/backlog-jira.ts +347 -0
- package/.claude/lib/backlog-sync.ts +42 -0
- package/.claude/lib/backlog.ts +396 -0
- package/.claude/lib/bazel.ts +212 -0
- package/.claude/lib/config.ts +122 -0
- package/.claude/lib/discovery.ts +821 -0
- package/.claude/lib/engineering.ts +406 -0
- package/.claude/lib/nx.ts +129 -0
- package/.claude/lib/repo.ts +1194 -0
- package/.claude/lib/toolgraph.ts +74 -0
- package/.claude/lib/turbo.ts +166 -0
- package/.claude/package-lock.json +427 -0
- package/.claude/package.json +21 -0
- package/.claude/settings.json +82 -0
- package/.claude/skills/adopt/SKILL.md +283 -0
- package/.claude/skills/adopt/scripts/boundary-check.ts +156 -0
- package/.claude/skills/adopt/scripts/package-doc.ts +71 -0
- package/.claude/skills/adopt/scripts/repo-map.ts +123 -0
- package/.claude/skills/adopt/scripts/rule-doc.ts +80 -0
- package/.claude/skills/architect/ADR-FORMAT.md +97 -0
- package/.claude/skills/architect/SKILL.md +64 -0
- package/.claude/skills/architect/TECH-STACK.md +21 -0
- package/.claude/skills/backlog/SKILL.md +198 -0
- package/.claude/skills/backlog/scripts/backlog-pull.ts +45 -0
- package/.claude/skills/backlog/scripts/backlog-push.ts +38 -0
- package/.claude/skills/backlog/scripts/backlog-status.ts +32 -0
- package/.claude/skills/backlog/scripts/jira-import.ts +40 -0
- package/.claude/skills/backlog/scripts/jira-plan.ts +26 -0
- package/.claude/skills/backlog/scripts/jira-record.ts +41 -0
- package/.claude/skills/bootstrap/SKILL.md +119 -0
- package/.claude/skills/codebase-design/DEEPENING.md +37 -0
- package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/.claude/skills/codebase-design/SKILL.md +114 -0
- package/.claude/skills/domain-model/CONTEXT-FORMAT.md +47 -0
- package/.claude/skills/domain-model/SKILL.md +64 -0
- package/.claude/skills/domain-model/templates/data-model.md +29 -0
- package/.claude/skills/engineering-conventions/SKILL.md +31 -0
- package/.claude/skills/event-storming/SKILL.md +44 -0
- package/.claude/skills/event-storming/schema/events.schema.json +124 -0
- package/.claude/skills/event-storming/scripts/render-event-storming.ts +11 -0
- package/.claude/skills/event-storming/scripts/validate-events.ts +11 -0
- package/.claude/skills/event-storming/templates/events.example.yaml +91 -0
- package/.claude/skills/feature/SKILL.md +86 -0
- package/.claude/skills/feature/scripts/intake.ts +356 -0
- package/.claude/skills/grill-me/SKILL.md +7 -0
- package/.claude/skills/grill-with-docs/SKILL.md +7 -0
- package/.claude/skills/grilling/SKILL.md +30 -0
- package/.claude/skills/groom-backlog/SKILL.md +93 -0
- package/.claude/skills/groom-backlog/scripts/backlog-health.ts +238 -0
- package/.claude/skills/implement/SKILL.md +129 -0
- package/.claude/skills/implement/scripts/affected.ts +245 -0
- package/.claude/skills/implement/scripts/check-commands.ts +37 -0
- package/.claude/skills/implement/scripts/check-language.ts +15 -0
- package/.claude/skills/implement/scripts/check-scenarios.ts +59 -0
- package/.claude/skills/implement/scripts/next-story.ts +23 -0
- package/.claude/skills/implement/scripts/story-context.ts +135 -0
- package/.claude/skills/implement/scripts/story-status.ts +47 -0
- package/.claude/skills/implement/scripts/verify.ts +92 -0
- package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/.claude/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/.claude/skills/retro/SKILL.md +53 -0
- package/.claude/skills/review-stories/SKILL.md +31 -0
- package/.claude/skills/shape/SKILL.md +193 -0
- package/.claude/skills/tdd/SKILL.md +38 -0
- package/.claude/skills/tdd/mocking.md +59 -0
- package/.claude/skills/tdd/tests.md +77 -0
- package/.claude/skills/user-stories/SKILL.md +138 -0
- package/.claude/skills/user-stories/schema/story.schema.json +39 -0
- package/.claude/skills/user-stories/scripts/render-backlog.ts +4 -0
- package/.claude/skills/user-stories/scripts/validate-stories.ts +13 -0
- package/.claude/skills/user-stories/templates/story-template.md +38 -0
- package/.claude/skills/ux-design/SKILL.md +107 -0
- package/.claude/skills/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/.claude/skills/writing-for-agents/SKILL.md +81 -0
- package/.claude/tsconfig.json +15 -0
- package/.opencode/agents/acceptance-tester.md +64 -0
- package/.opencode/agents/code-reviewer.md +69 -0
- package/.opencode/agents/domain-architect.md +59 -0
- package/.opencode/agents/lead-dev.md +67 -0
- package/.opencode/agents/product-owner.md +59 -0
- package/.opencode/agents/repo-analyst.md +86 -0
- package/.opencode/agents/requirements-engineer.md +58 -0
- package/.opencode/agents/ux-designer.md +83 -0
- package/.opencode/commands/adopt.md +7 -0
- package/.opencode/commands/backlog-sync.md +7 -0
- package/.opencode/commands/bootstrap.md +7 -0
- package/.opencode/commands/event-storming.md +7 -0
- package/.opencode/commands/feature.md +7 -0
- package/.opencode/commands/grill-me.md +7 -0
- package/.opencode/commands/grill-with-docs.md +7 -0
- package/.opencode/commands/groom-backlog.md +7 -0
- package/.opencode/commands/implement.md +7 -0
- package/.opencode/commands/improve-codebase-architecture.md +7 -0
- package/.opencode/commands/retro.md +7 -0
- package/.opencode/commands/review-stories.md +7 -0
- package/.opencode/commands/shape.md +7 -0
- package/.opencode/opencode.jsonc +15 -0
- package/.opencode/plugins/kataster/index.ts +283 -0
- package/AGENTS.md +21 -0
- package/CHANGELOG.md +91 -0
- package/LICENSE +21 -0
- package/README.md +331 -2
- package/bin/kataster.mjs +7 -0
- package/extras/skills/shadcn/SKILL.md +277 -0
- package/extras/skills/shadcn/agents/openai.yml +5 -0
- package/extras/skills/shadcn/assets/shadcn-small.png +0 -0
- package/extras/skills/shadcn/assets/shadcn.png +0 -0
- package/extras/skills/shadcn/cli.md +290 -0
- package/extras/skills/shadcn/customization.md +209 -0
- package/extras/skills/shadcn/evals/evals.json +77 -0
- package/extras/skills/shadcn/mcp.md +105 -0
- package/extras/skills/shadcn/registry.md +277 -0
- package/extras/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/extras/skills/shadcn/rules/chat.md +250 -0
- package/extras/skills/shadcn/rules/composition.md +213 -0
- package/extras/skills/shadcn/rules/forms.md +192 -0
- package/extras/skills/shadcn/rules/icons.md +101 -0
- package/extras/skills/shadcn/rules/styling.md +185 -0
- package/guide/README.md +9 -0
- package/guide/brownfield.md +242 -0
- package/guide/concepts.md +109 -0
- package/guide/greenfield.md +84 -0
- package/guide/install.md +144 -0
- package/guide/release.md +92 -0
- package/package.json +56 -4
- package/src/cli.mjs +302 -0
- package/src/detect.mjs +237 -0
- package/src/install.mjs +427 -0
- package/src/repo.mjs +129 -0
- package/src/util.mjs +110 -0
- package/templates/CONTEXT.md +21 -0
- package/templates/conventions.md +63 -0
- package/templates/docs/adr/README.md +7 -0
- package/templates/docs/architecture/data-model.md +31 -0
- package/templates/docs/domain/context-map.md +21 -0
- package/templates/docs/domain/events.yaml +23 -0
- package/templates/docs/product/initiatives/README.md +41 -0
- package/templates/docs/product/ux-guidelines.md +27 -0
- package/templates/docs/product/vision.md +36 -0
- package/templates/docs/reviews/.gitkeep +0 -0
- package/templates/docs/stories/OPEN_QUESTIONS.md +8 -0
- package/templates/docs/stories/README.md +26 -0
- package/templates/docs/workflow.md +135 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only intake for `/feature`.
|
|
3
|
+
*
|
|
4
|
+
* Turns a story id, a GitHub/Jira issue, a pasted URL or free text into a normalized source plus a routing hint.
|
|
5
|
+
* It never writes: no story, no mirror, no sync state. The skill decides what to do with the result.
|
|
6
|
+
*
|
|
7
|
+
* node .claude/skills/feature/scripts/intake.ts ST-042 --json
|
|
8
|
+
* node .claude/skills/feature/scripts/intake.ts #123 --json
|
|
9
|
+
* node .claude/skills/feature/scripts/intake.ts PROJ-123 --from jira-issues.json --json
|
|
10
|
+
* node .claude/skills/feature/scripts/intake.ts "customers attach photos to orders" --json
|
|
11
|
+
*
|
|
12
|
+
* Jira is reached through `mcp-atlassian`, which only the agent can call. The agent fetches the issue with MCP,
|
|
13
|
+
* writes it as JSON and passes it with `--from` – the same split the backlog sync uses.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { loadBacklogConfig, type BacklogConfig } from "../../../lib/config.ts";
|
|
17
|
+
import * as d from "../../../lib/discovery.ts";
|
|
18
|
+
import { externalOf, type RemoteIssue } from "../../../lib/backlog.ts";
|
|
19
|
+
import { readGithubIssue } from "../../../lib/backlog-github.ts";
|
|
20
|
+
import { normalizeJiraIssue } from "../../../lib/backlog-jira.ts";
|
|
21
|
+
|
|
22
|
+
type Route = "continue-story" | "review-story" | "implement-story" | "new-story" | "bug" | "feature";
|
|
23
|
+
type SourceKind = "story" | "github" | "jira" | "text";
|
|
24
|
+
|
|
25
|
+
interface Source {
|
|
26
|
+
kind: SourceKind;
|
|
27
|
+
ref: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
url?: string;
|
|
30
|
+
status?: string;
|
|
31
|
+
labels: string[];
|
|
32
|
+
body?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface StoryRef {
|
|
36
|
+
id: string;
|
|
37
|
+
path: string;
|
|
38
|
+
title: string;
|
|
39
|
+
status: string;
|
|
40
|
+
type: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Intake {
|
|
44
|
+
source: Source;
|
|
45
|
+
story: StoryRef | null;
|
|
46
|
+
route: Route;
|
|
47
|
+
reason: string;
|
|
48
|
+
signals: {
|
|
49
|
+
bug: boolean;
|
|
50
|
+
scenarios: number;
|
|
51
|
+
checklistItems: number;
|
|
52
|
+
openQuestions: number;
|
|
53
|
+
};
|
|
54
|
+
warnings: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const BUG_LABELS = new Set(["bug", "defect", "regression"]);
|
|
58
|
+
const GH_URL = /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/issues\/(\d+)(?:[?#].*)?$/i;
|
|
59
|
+
const JIRA_URL = /^https?:\/\/[^/\s]+\/browse\/([A-Z][A-Z0-9_]+-\d+)(?:[?#].*)?$/i;
|
|
60
|
+
|
|
61
|
+
function fail(message: string): never {
|
|
62
|
+
console.error(message);
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function count(re: RegExp, text: string): number {
|
|
67
|
+
return (text.match(re) ?? []).length;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A bug is a hint for the routing, never a verdict – the grilling decides. */
|
|
71
|
+
function looksLikeBug(title: string, labels: string[], issueType = ""): boolean {
|
|
72
|
+
if (labels.some((label) => BUG_LABELS.has(label.toLowerCase()))) return true;
|
|
73
|
+
if (/^bug$/i.test(issueType)) return true;
|
|
74
|
+
return /^\s*(?:\[bug\]|bug\b|fix\b)/i.test(title);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function findStory(id: string): d.Story | null {
|
|
78
|
+
const wanted = id.toUpperCase();
|
|
79
|
+
return d.loadStories().parsed.find((story) => d.storyId(story).toUpperCase() === wanted) ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findLinked(kind: "github" | "jira", key: string): d.Story | null {
|
|
83
|
+
const wanted = kind === "github" ? key : key.toUpperCase();
|
|
84
|
+
return d.loadStories().parsed.find((story) => {
|
|
85
|
+
const ext = externalOf(story);
|
|
86
|
+
if (!ext || ext.provider !== kind) return false;
|
|
87
|
+
return (kind === "github" ? ext.key : ext.key.toUpperCase()) === wanted;
|
|
88
|
+
}) ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function storyRef(story: d.Story): StoryRef {
|
|
92
|
+
return {
|
|
93
|
+
id: d.storyId(story),
|
|
94
|
+
path: d.rel(story.path),
|
|
95
|
+
title: String(story.meta.title ?? ""),
|
|
96
|
+
status: String(story.meta.status ?? ""),
|
|
97
|
+
type: String(story.meta.type ?? "story"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function routeFor(story: d.Story | null, bug: boolean): { route: Route; reason: string } {
|
|
102
|
+
if (story) {
|
|
103
|
+
const id = d.storyId(story);
|
|
104
|
+
switch (String(story.meta.status ?? "")) {
|
|
105
|
+
case "draft":
|
|
106
|
+
return { route: "continue-story", reason: `${id} is a draft – revise it instead of creating a parallel feature` };
|
|
107
|
+
case "review":
|
|
108
|
+
return { route: "review-story", reason: `${id} is waiting for its review – run /review-stories ${id} or revise it` };
|
|
109
|
+
case "ready":
|
|
110
|
+
return { route: "implement-story", reason: `${id} is ready – continue with /implement ${id}` };
|
|
111
|
+
case "in-progress":
|
|
112
|
+
return { route: "implement-story", reason: `${id} is in progress – continue with /implement ${id}` };
|
|
113
|
+
default: {
|
|
114
|
+
const status = String(story.meta.status ?? "not open");
|
|
115
|
+
return {
|
|
116
|
+
route: "new-story",
|
|
117
|
+
reason: `${id} is ${status} – finished work is final, so this becomes a new story`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (bug) {
|
|
123
|
+
return { route: "bug", reason: "flagged as a bug – restore documented behaviour, do not run feature discovery" };
|
|
124
|
+
}
|
|
125
|
+
return { route: "feature", reason: "new or changed behaviour – run the feature pipeline" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function loadJiraIssues(path: string): { issue: RemoteIssue; raw: d.Obj }[] {
|
|
129
|
+
let raw: unknown;
|
|
130
|
+
try {
|
|
131
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
fail(`Jira: could not read ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
}
|
|
135
|
+
const list = Array.isArray(raw)
|
|
136
|
+
? raw
|
|
137
|
+
: (raw as d.Obj | null)?.issues ?? (raw as d.Obj | null)?.values ?? (raw as d.Obj | null)?.results ?? [];
|
|
138
|
+
if (!Array.isArray(list)) fail(`Jira: ${path} must contain an array of issues (or { "issues": [...] })`);
|
|
139
|
+
return list.flatMap((item) => {
|
|
140
|
+
if (item === null || typeof item !== "object") return [];
|
|
141
|
+
const issue = normalizeJiraIssue(item as d.Obj);
|
|
142
|
+
return issue ? [{ issue, raw: item as d.Obj }] : [];
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function jiraIssueType(raw: d.Obj): string {
|
|
147
|
+
const fields = (raw.fields ?? raw) as d.Obj;
|
|
148
|
+
const type = fields.issuetype ?? fields.issue_type ?? raw.issuetype;
|
|
149
|
+
if (typeof type === "string") return type;
|
|
150
|
+
return typeof type?.name === "string" ? type.name : "";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function summarize(source: Source, body: string): Pick<Intake, "signals"> {
|
|
154
|
+
return {
|
|
155
|
+
signals: {
|
|
156
|
+
bug: looksLikeBug(source.title ?? "", source.labels),
|
|
157
|
+
scenarios: count(/\bScenario(?: Outline| Template)?\s*:/g, body),
|
|
158
|
+
checklistItems: count(/^\s*[-*]\s*\[[ xX]\]/gm, body),
|
|
159
|
+
openQuestions: count(/\[OPEN\]/g, body),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface JiraHit {
|
|
165
|
+
source: Source;
|
|
166
|
+
linked: d.Story | null;
|
|
167
|
+
body: string;
|
|
168
|
+
jiraType: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function loadJira(key: string, from: string, cfg: BacklogConfig, warnings: string[]): JiraHit {
|
|
172
|
+
if (!from) {
|
|
173
|
+
fail(
|
|
174
|
+
`Jira ${key}: Jira is reached through mcp-atlassian, which only the agent can call.\n` +
|
|
175
|
+
" Fetch the issue with jira_get_issue, write it as JSON, then:\n" +
|
|
176
|
+
` node .claude/skills/feature/scripts/intake.ts ${key} --from <issue.json>`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const issues = loadJiraIssues(from);
|
|
180
|
+
const hit = issues.find((entry) => entry.issue.key.toUpperCase() === key) ??
|
|
181
|
+
(issues.length === 1 ? issues[0] : undefined);
|
|
182
|
+
if (!hit) {
|
|
183
|
+
fail(
|
|
184
|
+
`Jira ${key}: no matching issue in ${from}` +
|
|
185
|
+
`${issues.length ? ` (found ${issues.map((i) => i.issue.key).join(", ")})` : ""}.`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (cfg.provider !== "jira") {
|
|
189
|
+
warnings.push(`configured backlog provider is '${cfg.provider}' – the issue is read as evidence only`);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
source: {
|
|
193
|
+
kind: "jira",
|
|
194
|
+
ref: hit.issue.key,
|
|
195
|
+
title: hit.issue.title,
|
|
196
|
+
url: hit.issue.url,
|
|
197
|
+
status: hit.issue.status,
|
|
198
|
+
labels: hit.issue.labels,
|
|
199
|
+
body: hit.issue.body,
|
|
200
|
+
},
|
|
201
|
+
linked: findLinked("jira", hit.issue.key),
|
|
202
|
+
body: hit.issue.body,
|
|
203
|
+
jiraType: jiraIssueType(hit.raw),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function main(): void {
|
|
208
|
+
const args = process.argv.slice(2);
|
|
209
|
+
const json = args.includes("--json");
|
|
210
|
+
const fromIndex = args.indexOf("--from");
|
|
211
|
+
const from = fromIndex >= 0 ? args[fromIndex + 1] ?? "" : "";
|
|
212
|
+
const positional: string[] = [];
|
|
213
|
+
for (let i = 0; i < args.length; i++) {
|
|
214
|
+
if (args[i] === "--json") continue;
|
|
215
|
+
if (args[i] === "--from") {
|
|
216
|
+
i++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
positional.push(args[i]);
|
|
220
|
+
}
|
|
221
|
+
const input = positional.join(" ").trim();
|
|
222
|
+
|
|
223
|
+
if (!input) {
|
|
224
|
+
fail(
|
|
225
|
+
"Usage: node .claude/skills/feature/scripts/intake.ts <ST-NNN | #issue | PROJ-123 | URL | idea> " +
|
|
226
|
+
"[--from <jira-issues.json>] [--json]",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const cfg: BacklogConfig = loadBacklogConfig();
|
|
231
|
+
const warnings: string[] = [];
|
|
232
|
+
let source: Source;
|
|
233
|
+
let body = "";
|
|
234
|
+
let linked: d.Story | null = null;
|
|
235
|
+
let jiraType = "";
|
|
236
|
+
|
|
237
|
+
const storyMatch = /^(ST-\d{3,})$/i.exec(input);
|
|
238
|
+
const ghUrl = GH_URL.exec(input);
|
|
239
|
+
const jiraUrl = JIRA_URL.exec(input);
|
|
240
|
+
const ghRef = /^#(\d+)$/.exec(input);
|
|
241
|
+
const jiraRef = /^([A-Z][A-Z0-9_]+-\d+)$/.exec(input);
|
|
242
|
+
|
|
243
|
+
if (storyMatch) {
|
|
244
|
+
const id = storyMatch[1].toUpperCase();
|
|
245
|
+
const story = findStory(id);
|
|
246
|
+
if (story) {
|
|
247
|
+
linked = story;
|
|
248
|
+
body = story.body;
|
|
249
|
+
source = {
|
|
250
|
+
kind: "story",
|
|
251
|
+
ref: id,
|
|
252
|
+
title: String(story.meta.title ?? ""),
|
|
253
|
+
status: String(story.meta.status ?? ""),
|
|
254
|
+
labels: Array.isArray(story.meta.labels) ? story.meta.labels.map(String) : [],
|
|
255
|
+
body: story.body,
|
|
256
|
+
};
|
|
257
|
+
} else if (from) {
|
|
258
|
+
// `ST-` is the story prefix, but a Jira project may use the same key – the export disambiguates.
|
|
259
|
+
const hit = loadJira(id, from, cfg, warnings);
|
|
260
|
+
source = hit.source;
|
|
261
|
+
linked = hit.linked;
|
|
262
|
+
body = hit.body;
|
|
263
|
+
jiraType = hit.jiraType;
|
|
264
|
+
} else {
|
|
265
|
+
fail(`Story ${id} not found in docs/stories/.`);
|
|
266
|
+
}
|
|
267
|
+
} else if (ghRef || ghUrl) {
|
|
268
|
+
const key = ghRef ? ghRef[1] : ghUrl![3];
|
|
269
|
+
const repo = ghUrl ? `${ghUrl[1]}/${ghUrl[2]}` : undefined;
|
|
270
|
+
let issue: RemoteIssue | null;
|
|
271
|
+
try {
|
|
272
|
+
issue = readGithubIssue(cfg, key, repo);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
275
|
+
if (/ENOENT|spawnSync gh/i.test(message)) fail("GitHub: the `gh` CLI is not installed or not on PATH.");
|
|
276
|
+
fail(`GitHub: ${message}`);
|
|
277
|
+
}
|
|
278
|
+
if (!issue) fail(`GitHub issue #${key}${repo ? ` in ${repo}` : ""} not found (or not accessible).`);
|
|
279
|
+
if (cfg.provider !== "github") warnings.push(`configured backlog provider is '${cfg.provider}' – the issue is read as evidence only`);
|
|
280
|
+
if (issue.state === "closed") warnings.push("the issue is closed – confirm the work is still wanted");
|
|
281
|
+
body = issue.body;
|
|
282
|
+
source = {
|
|
283
|
+
kind: "github",
|
|
284
|
+
ref: `#${issue.key}`,
|
|
285
|
+
title: issue.title,
|
|
286
|
+
url: issue.url,
|
|
287
|
+
status: issue.state,
|
|
288
|
+
labels: issue.labels,
|
|
289
|
+
body: issue.body,
|
|
290
|
+
};
|
|
291
|
+
linked = findLinked("github", issue.key);
|
|
292
|
+
} else if (jiraRef || jiraUrl) {
|
|
293
|
+
const key = (jiraRef ? jiraRef[1] : jiraUrl![1]).toUpperCase();
|
|
294
|
+
const hit = loadJira(key, from, cfg, warnings);
|
|
295
|
+
source = hit.source;
|
|
296
|
+
linked = hit.linked;
|
|
297
|
+
body = hit.body;
|
|
298
|
+
jiraType = hit.jiraType;
|
|
299
|
+
} else {
|
|
300
|
+
body = input;
|
|
301
|
+
source = {
|
|
302
|
+
kind: "text",
|
|
303
|
+
ref: input,
|
|
304
|
+
title: input.split("\n")[0].slice(0, 120),
|
|
305
|
+
labels: [],
|
|
306
|
+
body: input,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const { signals } = summarize(source, body);
|
|
311
|
+
if (jiraType && /^bug$/i.test(jiraType)) signals.bug = true;
|
|
312
|
+
|
|
313
|
+
if (linked && source.kind !== "story") {
|
|
314
|
+
warnings.push(`linked to ${d.storyId(linked)} – run /backlog-sync pull first so the mirror is current`);
|
|
315
|
+
}
|
|
316
|
+
const broken = d.loadStories().failures.length;
|
|
317
|
+
if (broken) warnings.push(`${broken} story file(s) have broken frontmatter – run the story validator`);
|
|
318
|
+
|
|
319
|
+
const { route, reason } = routeFor(linked, signals.bug);
|
|
320
|
+
const result: Intake = {
|
|
321
|
+
source,
|
|
322
|
+
story: linked ? storyRef(linked) : null,
|
|
323
|
+
route,
|
|
324
|
+
reason,
|
|
325
|
+
signals,
|
|
326
|
+
warnings,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
if (json) {
|
|
330
|
+
console.log(JSON.stringify(result, null, 2));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const lines = [`Source ${source.kind} ${source.ref}${source.title ? ` – "${source.title}"` : ""}`];
|
|
335
|
+
if (source.url) lines.push(`URL ${source.url}`);
|
|
336
|
+
if (source.status) lines.push(`Status ${source.status}`);
|
|
337
|
+
if (source.labels.length) lines.push(`Labels ${source.labels.join(", ")}`);
|
|
338
|
+
if (result.story) {
|
|
339
|
+
lines.push(`Story ${result.story.id} "${result.story.title}" (${result.story.status})`);
|
|
340
|
+
lines.push(`Path ${result.story.path}`);
|
|
341
|
+
}
|
|
342
|
+
lines.push(`Route ${route}`, ` ${reason}`);
|
|
343
|
+
lines.push(
|
|
344
|
+
`Signals bug=${signals.bug ? "yes" : "no"} scenarios=${signals.scenarios} ` +
|
|
345
|
+
`checklist=${signals.checklistItems} open=${signals.openQuestions}`,
|
|
346
|
+
);
|
|
347
|
+
for (const warning of warnings) lines.push(`Warning ${warning}`);
|
|
348
|
+
console.log(lines.join("\n"));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
main();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grill-with-docs
|
|
3
|
+
description: A relentless interview to sharpen a plan or design, which also creates docs (glossary in CONTEXT.md and ADRs in docs/adr/) as we go.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Call the `skill` tool three times, for `grilling`, `domain-model` and `architect`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grilling
|
|
3
|
+
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
|
|
7
|
+
|
|
8
|
+
Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
|
|
9
|
+
|
|
10
|
+
Format a round like so:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
|
14
|
+
|
|
15
|
+
➡️ <your recommended answer>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
❓ **Q2** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
|
|
20
|
+
|
|
21
|
+
➡️ <your recommended answer>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Word each question so "yes" accepts your recommended answer.
|
|
25
|
+
|
|
26
|
+
Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
|
|
27
|
+
|
|
28
|
+
Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait.
|
|
29
|
+
|
|
30
|
+
The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: groom-backlog
|
|
3
|
+
description: Periodic grooming of the open backlog – challenges whether stories are still worth building, whether dependencies still hold and whether the priorities still match what the product needs now.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
argument-hint: "[BC-Name | ST-IDs, empty = every story that is not done]"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Backlog Grooming
|
|
9
|
+
|
|
10
|
+
Scope: the arguments from the invocation (empty → every story that is not `done`; `BC-Collection` → one bounded context; `ST-007 ST-008` → single stories).
|
|
11
|
+
|
|
12
|
+
`/review-stories` asks "is this new story ready to build?". Grooming asks the opposite question about what has been
|
|
13
|
+
lying around: **would we still write this story today?** The backlog was derived in one go from the event storming;
|
|
14
|
+
every done story, every ADR and every answered open question since then may have made part of it wrong, redundant
|
|
15
|
+
or differently important. Nothing here is a formality – a grooming run that changes nothing was a wasted run or a
|
|
16
|
+
healthy backlog, and you have to be able to say which.
|
|
17
|
+
|
|
18
|
+
Do **not** fix anything yourself. Grooming produces a decision for the user and a task for the `requirements-engineer`.
|
|
19
|
+
|
|
20
|
+
## 1. Mechanical health check
|
|
21
|
+
|
|
22
|
+
With a tracker (`backlog.provider` ≠ `local`), pull first (`/backlog-sync pull`) – grooming a stale mirror wastes the
|
|
23
|
+
run. Then:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
node .claude/skills/groom-backlog/scripts/backlog-health.ts <scope>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Dependency cycles, stories waiting for dropped or non-existent ones, priority inversions, `ready` stories blocked
|
|
30
|
+
by `draft`, references to IDs `events.yaml` no longer has, stories that already have tests, parked `[OPEN]`
|
|
31
|
+
decisions and open questions older than 30 days. These are **findings, not errors** – they say where to look, the
|
|
32
|
+
judgement comes next. Read `docs/stories/BACKLOG.md` and `docs/stories/OPEN_QUESTIONS.md` as well.
|
|
33
|
+
|
|
34
|
+
## 2. What changed since the backlog was written
|
|
35
|
+
|
|
36
|
+
Before asking anyone else: build the list of changes the backlog may not have caught up with. `git log --oneline`
|
|
37
|
+
since the last grooming review in `docs/reviews/`, the stories that went `done`, new and superseded ADRs, answered
|
|
38
|
+
rows in `OPEN_QUESTIONS.md`, and the "revisit when …" lines parked in `docs/reviews/ST-NNN-code-review.md`. Hand
|
|
39
|
+
this list to the subagents – they cannot see it, and without it they re-read the same stories that were already
|
|
40
|
+
reviewed once.
|
|
41
|
+
|
|
42
|
+
## 3. Challenge in parallel
|
|
43
|
+
|
|
44
|
+
Start in **one** message, with the full file paths, the scope and the change list from step 2:
|
|
45
|
+
|
|
46
|
+
- `product-owner` – is the value still real and still in the vision? Is the story already covered by what was
|
|
47
|
+
built? Does the MVP slice still hold? Which `must` is not a `must` any more, and which `could` turned out to be
|
|
48
|
+
one? Which need from the vision still has no story?
|
|
49
|
+
- `lead-dev` – do `depends_on`, `size` and `risk` still fit **now that the code exists**? Which story has become
|
|
50
|
+
smaller because the foundation is there, which bigger? Which dependency is technical fiction? Which story should
|
|
51
|
+
be split, merged or dropped?
|
|
52
|
+
- `ux-designer` – only for stories with the label `ui` (or an obvious frontend part): does the story still match
|
|
53
|
+
`docs/product/ux-guidelines.md`, and which one builds a screen that a newer story has replaced?
|
|
54
|
+
|
|
55
|
+
Tell each one explicitly: **recommend, don't rewrite** – they change only the frontmatter fields they own
|
|
56
|
+
(`priority` for the PO, `size`/`risk`/`depends_on` for the lead dev), everything else comes back as a proposal.
|
|
57
|
+
|
|
58
|
+
## 4. Consolidate
|
|
59
|
+
|
|
60
|
+
Write `docs/reviews/YYYY-MM-DD-backlog-grooming.md`:
|
|
61
|
+
|
|
62
|
+
- the health-check findings with what they turned out to mean
|
|
63
|
+
- per challenged story: keep / revise / split / merge / drop, with the reason and who said it
|
|
64
|
+
- conflicts between the three – name them, don't average them away
|
|
65
|
+
- proposed new stories, and stories proposed for `priority: wont`
|
|
66
|
+
- what explicitly stays as it is, so the next run does not re-litigate it
|
|
67
|
+
|
|
68
|
+
## 5. The user decides
|
|
69
|
+
|
|
70
|
+
Present it in rounds, at most 3 questions at a time, each with a recommended answer. Adopt nothing without consent.
|
|
71
|
+
A disagreement between PO and lead-dev is a question for the user, not something to resolve on their behalf.
|
|
72
|
+
|
|
73
|
+
## 6. Apply
|
|
74
|
+
|
|
75
|
+
- Content changes → `requirements-engineer`, referencing the grooming file.
|
|
76
|
+
- **Dropped stories are never deleted**: they get `priority: wont` and keep their file, so the decision stays
|
|
77
|
+
traceable. A story that is wrong rather than unwanted goes back to `status: review` and through `/review-stories`.
|
|
78
|
+
- A `done` story is never reopened – a change to finished behaviour is a new story.
|
|
79
|
+
- New stories from the grooming get `status: draft` and go through `/review-stories` like any other.
|
|
80
|
+
- With a tracker, push the applied changes (`/backlog-sync push`) before the wrap-up, so Jira/GitHub and the mirror
|
|
81
|
+
agree. A story that is not linked yet stays a local draft until the push creates it.
|
|
82
|
+
|
|
83
|
+
## 7. Wrap-up
|
|
84
|
+
|
|
85
|
+
`node .claude/skills/user-stories/scripts/validate-stories.ts`, then the health check again – the findings you
|
|
86
|
+
decided to act on should be gone, and you should be able to say why each remaining one stays. Summarize in a few
|
|
87
|
+
lines: how many stories were challenged, what changed, what was dropped, what the user still has to decide.
|
|
88
|
+
|
|
89
|
+
## When to run it
|
|
90
|
+
|
|
91
|
+
After a bounded context is finished, after a `/feature` has landed, or roughly every 8–10 done domain stories –
|
|
92
|
+
and always before planning a larger block of work. Not between two stories of the same context: the backlog has
|
|
93
|
+
not moved by then, and a grooming run that finds nothing twice in a row means the interval is too short.
|