kataster 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +82 -0
- package/LICENSE +21 -0
- package/README.md +324 -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 +62 -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,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jira adapter.
|
|
3
|
+
*
|
|
4
|
+
* Jira is reached through the `mcp-atlassian` MCP server, which only the agent can call – a plain Node script cannot
|
|
5
|
+
* speak MCP. This module therefore does the deterministic half:
|
|
6
|
+
*
|
|
7
|
+
* - `jiraPlan` turns the local stories into exact MCP calls (`jira_create_issue`, `jira_update_issue`,
|
|
8
|
+
* `jira_transition_issue`, `jira_create_issue_link`)
|
|
9
|
+
* - `jiraRecord` writes the issue keys the agent got back into the story frontmatter
|
|
10
|
+
* - `jiraImport` rebuilds the local mirror from the issue data the agent fetched with `jira_search`/`jira_get_issue`
|
|
11
|
+
*
|
|
12
|
+
* The skill `backlog` owns the order of those steps. Nothing here talks to the network.
|
|
13
|
+
*/
|
|
14
|
+
import * as d from "./discovery.ts";
|
|
15
|
+
import type { BacklogConfig } from "./config.ts";
|
|
16
|
+
import {
|
|
17
|
+
backupStory,
|
|
18
|
+
bumpStoryId,
|
|
19
|
+
externalOf,
|
|
20
|
+
knownContexts,
|
|
21
|
+
nextStoryId,
|
|
22
|
+
parseIssue,
|
|
23
|
+
payloadHash,
|
|
24
|
+
readProviderState,
|
|
25
|
+
remotePayload,
|
|
26
|
+
renderIssue,
|
|
27
|
+
setExternal,
|
|
28
|
+
storyHash,
|
|
29
|
+
writeStory,
|
|
30
|
+
writeSyncState,
|
|
31
|
+
type RemoteIssue,
|
|
32
|
+
} from "./backlog.ts";
|
|
33
|
+
|
|
34
|
+
export interface JiraAction {
|
|
35
|
+
story: string;
|
|
36
|
+
action: "create" | "update" | "transition" | "link" | "skip";
|
|
37
|
+
tool: string;
|
|
38
|
+
arguments: Record<string, unknown>;
|
|
39
|
+
note?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface JiraPlan {
|
|
43
|
+
provider: "jira";
|
|
44
|
+
project: string;
|
|
45
|
+
issueType: string;
|
|
46
|
+
mcpServer: string;
|
|
47
|
+
actions: JiraAction[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const list = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []);
|
|
51
|
+
|
|
52
|
+
function additionalFields(payload: { labels: string[] }, priority: unknown, cfg: BacklogConfig): string {
|
|
53
|
+
const fields: Record<string, unknown> = { labels: payload.labels };
|
|
54
|
+
const priorityName = cfg.jira.priorityMap[String(priority)] ?? undefined;
|
|
55
|
+
if (priorityName) fields.priority = { name: priorityName };
|
|
56
|
+
return JSON.stringify(fields);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The exact MCP calls that bring Jira in line with the local mirror (one direction: local → Jira). */
|
|
60
|
+
export function jiraPlan(cfg: BacklogConfig, ids: string[] = []): JiraPlan {
|
|
61
|
+
if (!cfg.jira.project) throw new Error("backlog.jira.project is empty in .claude/project.json");
|
|
62
|
+
const wanted = new Set(ids.map((id) => id.toUpperCase()));
|
|
63
|
+
const allStories = d.loadStories().parsed;
|
|
64
|
+
const stories = allStories.filter((story) => !wanted.size || wanted.has(d.storyId(story)));
|
|
65
|
+
const byId = new Map(allStories.map((story) => [d.storyId(story), story]));
|
|
66
|
+
const actions: JiraAction[] = [];
|
|
67
|
+
|
|
68
|
+
for (const story of stories) {
|
|
69
|
+
const id = d.storyId(story);
|
|
70
|
+
const payload = renderIssue(story, cfg);
|
|
71
|
+
const ext = externalOf(story);
|
|
72
|
+
if (ext && ext.provider !== "jira") {
|
|
73
|
+
actions.push({
|
|
74
|
+
story: id,
|
|
75
|
+
action: "skip",
|
|
76
|
+
tool: "",
|
|
77
|
+
arguments: {},
|
|
78
|
+
note: `Linked to ${ext.provider} (${ext.key}); change backlog.provider or remove the external ref.`,
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const target = cfg.jira.statusMap[String(story.meta.status)] ?? String(story.meta.status);
|
|
83
|
+
const priority = story.meta.priority;
|
|
84
|
+
|
|
85
|
+
if (!ext) {
|
|
86
|
+
actions.push({
|
|
87
|
+
story: id,
|
|
88
|
+
action: "create",
|
|
89
|
+
tool: "jira_create_issue",
|
|
90
|
+
arguments: {
|
|
91
|
+
project_key: cfg.jira.project,
|
|
92
|
+
issue_type: cfg.jira.issueType,
|
|
93
|
+
summary: payload.title,
|
|
94
|
+
description: payload.body,
|
|
95
|
+
additional_fields: additionalFields(payload, priority, cfg),
|
|
96
|
+
},
|
|
97
|
+
note: `Record the returned key with jira-record.ts, then run jira-plan.ts again for the transition to '${target}' and the links.`,
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
actions.push({
|
|
101
|
+
story: id,
|
|
102
|
+
action: "update",
|
|
103
|
+
tool: "jira_update_issue",
|
|
104
|
+
arguments: {
|
|
105
|
+
issue_key: ext.key,
|
|
106
|
+
fields: JSON.stringify({ summary: payload.title, description: payload.body }),
|
|
107
|
+
additional_fields: additionalFields(payload, priority, cfg),
|
|
108
|
+
return_fields: "summary,status,labels,priority",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
actions.push({
|
|
112
|
+
story: id,
|
|
113
|
+
action: "transition",
|
|
114
|
+
tool: "jira_transition_issue",
|
|
115
|
+
arguments: { issue_key: ext.key, transition_id: target },
|
|
116
|
+
note: `Run jira_get_transitions first; use the transition that reaches '${target}'. Skip if Jira is already there.`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const dep of list(story.meta.depends_on)) {
|
|
121
|
+
const dependency = byId.get(dep);
|
|
122
|
+
const depExt = dependency ? externalOf(dependency) : null;
|
|
123
|
+
if (!ext) {
|
|
124
|
+
actions.push({
|
|
125
|
+
story: id,
|
|
126
|
+
action: "skip",
|
|
127
|
+
tool: "",
|
|
128
|
+
arguments: {},
|
|
129
|
+
note: `Depends on ${dep}; create ${id} first, then run jira-plan.ts again for the link.`,
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!depExt) {
|
|
134
|
+
actions.push({
|
|
135
|
+
story: id,
|
|
136
|
+
action: "skip",
|
|
137
|
+
tool: "",
|
|
138
|
+
arguments: {},
|
|
139
|
+
note: `Depends on ${dep}, which has no Jira key yet; create/link ${dep} first, then run jira-plan.ts again.`,
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
actions.push({
|
|
144
|
+
story: id,
|
|
145
|
+
action: "link",
|
|
146
|
+
tool: "jira_create_issue_link",
|
|
147
|
+
arguments: {
|
|
148
|
+
link_type: "Blocks",
|
|
149
|
+
outward_issue_key: depExt.key,
|
|
150
|
+
inward_issue_key: ext.key,
|
|
151
|
+
},
|
|
152
|
+
note: `${depExt.key} (${dep}) blocks ${ext.key} (${id}).`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
provider: "jira",
|
|
159
|
+
project: cfg.jira.project,
|
|
160
|
+
issueType: cfg.jira.issueType,
|
|
161
|
+
mcpServer: cfg.jira.mcpServer,
|
|
162
|
+
actions,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface JiraRecord {
|
|
167
|
+
story: string;
|
|
168
|
+
key: string;
|
|
169
|
+
url?: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Writes the keys the agent got back from `jira_create_issue` into the story files. */
|
|
173
|
+
export function jiraRecord(cfg: BacklogConfig, results: JiraRecord[]): string[] {
|
|
174
|
+
const stories = new Map(d.loadStories().parsed.map((story) => [d.storyId(story), story]));
|
|
175
|
+
const state = readProviderState("jira");
|
|
176
|
+
const lines: string[] = [];
|
|
177
|
+
for (const result of results) {
|
|
178
|
+
const story = stories.get(result.story.toUpperCase());
|
|
179
|
+
if (!story) {
|
|
180
|
+
lines.push(`skip ${result.story}: no such story`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!result.key) {
|
|
184
|
+
lines.push(`skip ${result.story}: no Jira key`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
setExternal(story, { provider: "jira", key: result.key, ...(result.url ? { url: result.url } : {}) });
|
|
188
|
+
state.stories[d.storyId(story)] = {
|
|
189
|
+
key: result.key,
|
|
190
|
+
...(result.url ? { url: result.url } : {}),
|
|
191
|
+
localHash: storyHash(story),
|
|
192
|
+
remoteHash: "",
|
|
193
|
+
};
|
|
194
|
+
lines.push(`linked ${d.storyId(story)} → ${result.key}`);
|
|
195
|
+
}
|
|
196
|
+
state.provider = "jira";
|
|
197
|
+
if (lines.some((line) => line.startsWith("linked"))) writeSyncState(state);
|
|
198
|
+
return lines;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Accepts the shapes `jira_search` / `jira_get_issue` return, or the normalized shape the skill documents. */
|
|
202
|
+
export function normalizeJiraIssue(raw: d.Obj): RemoteIssue | null {
|
|
203
|
+
const fields = (raw.fields ?? raw) as d.Obj;
|
|
204
|
+
const key = raw.key ?? raw.issue_key ?? raw.issueKey;
|
|
205
|
+
if (typeof key !== "string" || !key) return null;
|
|
206
|
+
const status = typeof fields.status === "string" ? fields.status : fields.status?.name;
|
|
207
|
+
const priority = typeof fields.priority === "string" ? fields.priority : fields.priority?.name;
|
|
208
|
+
const labels = list(fields.labels ?? raw.labels);
|
|
209
|
+
const description = fields.description ?? raw.description ?? "";
|
|
210
|
+
return {
|
|
211
|
+
key,
|
|
212
|
+
url: typeof raw.browse_url === "string" ? raw.browse_url : typeof raw.url === "string" ? raw.url : undefined,
|
|
213
|
+
title: String(fields.summary ?? raw.summary ?? ""),
|
|
214
|
+
body: typeof description === "string" ? description : "",
|
|
215
|
+
labels,
|
|
216
|
+
status: typeof status === "string" ? status : undefined,
|
|
217
|
+
priority: typeof priority === "string" ? priority : undefined,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Rebuilds local stories from Jira issue data. The tracker wins; unpushed local changes are reported. */
|
|
222
|
+
export function jiraImport(cfg: BacklogConfig, rawIssues: d.Obj[]): { lines: string[]; changed: boolean } {
|
|
223
|
+
const issues = rawIssues.map(normalizeJiraIssue).filter((issue): issue is RemoteIssue => issue !== null);
|
|
224
|
+
const stories = d.loadStories().parsed;
|
|
225
|
+
const byId = new Map(stories.map((story) => [d.storyId(story), story]));
|
|
226
|
+
const byKey = new Map(
|
|
227
|
+
stories.flatMap((story) => {
|
|
228
|
+
const ext = externalOf(story);
|
|
229
|
+
return ext && ext.provider === "jira" ? [[ext.key, story] as const] : [];
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
const state = readProviderState("jira");
|
|
233
|
+
const contexts = knownContexts();
|
|
234
|
+
const lines: string[] = [];
|
|
235
|
+
let changed = false;
|
|
236
|
+
let next = nextStoryId();
|
|
237
|
+
|
|
238
|
+
for (const issue of issues) {
|
|
239
|
+
const existingByKey = byKey.get(issue.key);
|
|
240
|
+
const parsed = parseIssue(issue, cfg, {
|
|
241
|
+
contexts,
|
|
242
|
+
fallbackId: existingByKey ? d.storyId(existingByKey) : "",
|
|
243
|
+
});
|
|
244
|
+
// The story id is stable; an issue matched by its external key keeps the id it already has.
|
|
245
|
+
let id = existingByKey ? d.storyId(existingByKey) : parsed.id;
|
|
246
|
+
if (existingByKey && parsed.id && parsed.id !== id) {
|
|
247
|
+
lines.push(`warning ${id}: the issue title claims ${parsed.id}; the stable id stays ${id}`);
|
|
248
|
+
}
|
|
249
|
+
if (!id) {
|
|
250
|
+
id = next;
|
|
251
|
+
next = bumpStoryId(next);
|
|
252
|
+
}
|
|
253
|
+
const existing = byId.get(id) ?? existingByKey;
|
|
254
|
+
const existingExt = existing ? externalOf(existing) : null;
|
|
255
|
+
if (existingExt && (existingExt.provider !== "jira" || existingExt.key !== issue.key)) {
|
|
256
|
+
lines.push(`conflict ${id}: local story is linked to ${existingExt.provider} ${existingExt.key}, but ${issue.key} claims the same id – skipped`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!parsed.meta.context) {
|
|
260
|
+
lines.push(`warning ${issue.key} (${id}): no bounded context in the issue – fix it in Jira or the mirror will fail validation`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const remoteHash = payloadHash(remotePayload(issue, cfg));
|
|
264
|
+
const entry = state.stories[id];
|
|
265
|
+
const localHash = existing ? storyHash(existing) : "";
|
|
266
|
+
const remoteUnchanged = Boolean(entry && entry.remoteHash === remoteHash);
|
|
267
|
+
const localChanged = Boolean(existing && entry && localHash !== entry.localHash);
|
|
268
|
+
|
|
269
|
+
// Fresh clone: the state is not committed, the mirror is. Rebuild the state without touching the story.
|
|
270
|
+
if (existing && !entry && payloadHash(renderIssue(existing, cfg)) === remoteHash) {
|
|
271
|
+
state.stories[id] = {
|
|
272
|
+
key: issue.key,
|
|
273
|
+
...(issue.url ? { url: issue.url } : {}),
|
|
274
|
+
localHash,
|
|
275
|
+
remoteHash,
|
|
276
|
+
};
|
|
277
|
+
lines.push(`synced ${id} ← ${issue.key} (state rebuilt)`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (existing && remoteUnchanged && localChanged) {
|
|
282
|
+
lines.push(`local ${id}: local changes are not pushed yet – Jira did not change, left alone`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (existing && entry && localChanged && !remoteUnchanged) {
|
|
286
|
+
const backup = backupStory(existing);
|
|
287
|
+
lines.push(`conflict ${id}: local and Jira changed – the tracker wins, local copy saved to ${backup}`);
|
|
288
|
+
} else if (existing && !entry) {
|
|
289
|
+
const backup = backupStory(existing);
|
|
290
|
+
lines.push(`conflict ${id}: no sync state yet – the tracker wins, local copy saved to ${backup}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const path = writeStory(id, parsed.meta, parsed.body, existing);
|
|
294
|
+
state.stories[id] = {
|
|
295
|
+
key: issue.key,
|
|
296
|
+
...(issue.url ? { url: issue.url } : {}),
|
|
297
|
+
localHash: storyHash({ ...parsed, path } as d.Story),
|
|
298
|
+
remoteHash,
|
|
299
|
+
};
|
|
300
|
+
lines.push(`${existing ? "updated " : "imported"} ${id} ← ${issue.key} (${d.rel(path)})`);
|
|
301
|
+
changed = true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const importedKeys = new Set(issues.map((issue) => issue.key));
|
|
305
|
+
for (const story of stories) {
|
|
306
|
+
const ext = externalOf(story);
|
|
307
|
+
if (!ext || ext.provider !== "jira") continue;
|
|
308
|
+
if (!importedKeys.has(ext.key)) lines.push(`orphan ${d.storyId(story)}: linked to ${ext.key}, which was not in the import`);
|
|
309
|
+
}
|
|
310
|
+
for (const story of stories) {
|
|
311
|
+
if (!externalOf(story)) lines.push(`local ${d.storyId(story)}: not linked to Jira yet – run jira-plan and record the key`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
state.provider = "jira";
|
|
315
|
+
if (changed) writeSyncState(state);
|
|
316
|
+
if (changed) d.writeBacklogMd();
|
|
317
|
+
if (!lines.length) lines.push("nothing to import");
|
|
318
|
+
return { lines, changed };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** What can be said about the Jira side without calling MCP: which stories are linked, and which are local only. */
|
|
322
|
+
export function jiraStatus(cfg: BacklogConfig): { lines: string[]; drift: boolean } {
|
|
323
|
+
const state = readProviderState("jira");
|
|
324
|
+
const lines: string[] = [];
|
|
325
|
+
let drift = false;
|
|
326
|
+
for (const story of d.loadStories().parsed) {
|
|
327
|
+
const id = d.storyId(story);
|
|
328
|
+
const ext = externalOf(story);
|
|
329
|
+
if (!ext || ext.provider !== "jira") {
|
|
330
|
+
const committed = ["ready", "in-progress", "done"].includes(String(story.meta.status));
|
|
331
|
+
lines.push(`${committed ? "missing" : "local "} ${id}: ${committed ? "must be linked to Jira" : "not linked to Jira"}`);
|
|
332
|
+
if (committed) drift = true;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const entry = state.stories[id];
|
|
336
|
+
if (!entry) {
|
|
337
|
+
lines.push(`unknown ${id}: linked to ${ext.key}, but no sync state yet – run jira-import once`);
|
|
338
|
+
drift = true;
|
|
339
|
+
} else if (storyHash(story) !== entry.localHash) {
|
|
340
|
+
lines.push(`local ${id}: local changes are not pushed to ${ext.key}`);
|
|
341
|
+
drift = true;
|
|
342
|
+
} else {
|
|
343
|
+
lines.push(`linked ${id} ↔ ${ext.key} (Jira status not checked – use the MCP pull)`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { lines, drift };
|
|
347
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place for "the local story status changed – what does that mean for the tracker?".
|
|
3
|
+
*
|
|
4
|
+
* GitHub can be updated deterministically from here (a `gh` call). Jira cannot: the MCP server is only reachable
|
|
5
|
+
* from the agent, so the best a script can do is print the exact transition the agent has to perform. Neither
|
|
6
|
+
* failure may block the local workflow – the mirror stays usable, and the next sync reports the drift.
|
|
7
|
+
*/
|
|
8
|
+
import * as d from "./discovery.ts";
|
|
9
|
+
import { labelPrefix, loadBacklogConfig } from "./config.ts";
|
|
10
|
+
import { externalOf } from "./backlog.ts";
|
|
11
|
+
import { githubSetStatus } from "./backlog-github.ts";
|
|
12
|
+
|
|
13
|
+
export function syncStatusAfterChange(story: d.Story, status: string): string | null {
|
|
14
|
+
const cfg = loadBacklogConfig();
|
|
15
|
+
if (cfg.provider === "local") return null;
|
|
16
|
+
if (process.argv.includes("--no-tracker")) return null;
|
|
17
|
+
|
|
18
|
+
const ext = externalOf(story);
|
|
19
|
+
if (!ext) {
|
|
20
|
+
return `Backlog: ${cfg.provider} – ${d.storyId(story)} is not linked yet; run /backlog-sync push.`;
|
|
21
|
+
}
|
|
22
|
+
if (ext.provider !== cfg.provider) {
|
|
23
|
+
return `Backlog: ${d.storyId(story)} is linked to ${ext.provider} (${ext.key}), but the provider is ${cfg.provider} – fix .claude/project.json or the external ref.`;
|
|
24
|
+
}
|
|
25
|
+
if (cfg.provider === "jira") {
|
|
26
|
+
const target = cfg.jira.statusMap[status] ?? status;
|
|
27
|
+
return (
|
|
28
|
+
`Backlog: Jira ${ext.key} was not changed from here (MCP is agent-only). ` +
|
|
29
|
+
`Transition it to '${target}' with jira_get_transitions + jira_transition_issue, then /backlog-sync push.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return githubSetStatus(story, status, cfg);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return `Backlog: GitHub #${ext.key} could not be updated (${error instanceof Error ? error.message : String(error)}); run /backlog-sync push.`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Label prefix of the active provider – used by the session start hint. */
|
|
40
|
+
export function activeLabelPrefix(): string {
|
|
41
|
+
return labelPrefix(loadBacklogConfig());
|
|
42
|
+
}
|