stepwell 0.0.0-stage → 0.2.4

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/dist/tools.js ADDED
@@ -0,0 +1,551 @@
1
+ import { z } from "zod";
2
+ import { applyArchivePlan, applyBacklogAddPlan, applyBacklogRemovePlan, applyBacklogUpdatePlan, applyPhasePlan, applyProgressPlan, backlogShow, docsRepair, docsStatus, docsValidate, loadProject, planArchiveItem, planBacklogAdd, planBacklogRemove, planBacklogUpdate, planPhase, planProgressUpdate, ProjectNotInitializedError, readBacklog, readProgress, } from "stepwell-core";
3
+ import { commitDocs, docCommitMessage, foreignDocState, pullTrunk, pushTrunk, syncPreview, workstreamReport } from "stepwell-git";
4
+ function textResult(value) {
5
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
6
+ }
7
+ /** Text-Payload, bei Opt-in (`structured: true`) zusätzlich als structuredContent (M3/9.7, E1/12.1). */
8
+ function structuredResult(value) {
9
+ return {
10
+ content: [{ type: "text", text: JSON.stringify(value) }],
11
+ structuredContent: value,
12
+ };
13
+ }
14
+ function errorResult(err) {
15
+ const message = err instanceof Error ? err.message : String(err);
16
+ return { content: [{ type: "text", text: message }], isError: true };
17
+ }
18
+ function errorOrThrow(err) {
19
+ if (err instanceof ProjectNotInitializedError) {
20
+ return {
21
+ content: [{
22
+ type: "text",
23
+ text: JSON.stringify({ code: err.code, message: err.message, missing: err.missing }),
24
+ }],
25
+ isError: true,
26
+ };
27
+ }
28
+ return errorResult(err);
29
+ }
30
+ async function asResult(fn) {
31
+ try {
32
+ return textResult(fn());
33
+ }
34
+ catch (err) {
35
+ return errorOrThrow(err);
36
+ }
37
+ }
38
+ async function asStructuredResult(fn, structured) {
39
+ try {
40
+ const value = fn();
41
+ return structured ? structuredResult(value) : textResult(value);
42
+ }
43
+ catch (err) {
44
+ return errorOrThrow(err);
45
+ }
46
+ }
47
+ const PRIORITY_VALUES = ["🔴", "🟠", "🟡", "🟢", "🔵", "unknown"];
48
+ const PRIORITY_WRITE_VALUES = ["🔴", "🟠", "🟡", "🟢", "🔵"];
49
+ const TEXT_FIELD = z.string().optional()
50
+ .describe("Item body as bullets (e.g. \"- **Location:** …\\n- **Problem:** …\") — taken over verbatim.");
51
+ const DRYRUN_FIELD = z.boolean().default(true)
52
+ .describe("true (default): plan/diff preview only; false: write the changes.");
53
+ const STRUCTURED_FIELD = z.boolean().default(false)
54
+ .describe("true: payload additionally as structuredContent (M3); false (default): text content only.");
55
+ const DETAIL_FIELD = z.enum(["summary", "diff"]).default("diff")
56
+ .describe("Plan detail level for dry-runs: \"diff\" (default) = full preview; \"summary\" = " +
57
+ "headline + line counts (routine status maintenance). Apply responses are unchanged.");
58
+ const COMMIT_FIELD = z.boolean().default(false)
59
+ .describe("false (default): no git action. true: after a successful apply, commit ONLY the four doc " +
60
+ "files (deterministic staging — never code or other files; refused with COMMIT_BLOCKED when " +
61
+ "the index has pre-staged changes; a failed commit keeps the edits applied, code COMMIT_FAILED). " +
62
+ "Ignored for dry-runs. Result lands as a `commit` section in the response.");
63
+ const COMMIT_MESSAGE_FIELD = z.string().optional()
64
+ .describe("Optional commit subject with commit: true; default: generated from the operation.");
65
+ /** Attaches the commit section after a successful apply (never on a failed one). */
66
+ function attachCommit(result, root, message) {
67
+ if (result.verification?.ok === false)
68
+ return;
69
+ result.commit = commitDocs(root, { message });
70
+ }
71
+ /** Projiziert Dry-run-Pläne auf Headline + Zeilenzahlen (E1/12.2) — Apply bleibt unberührt. */
72
+ function summarizePlan(plan) {
73
+ if (plan.dryRun !== true)
74
+ return plan;
75
+ const changes = plan.changes;
76
+ if (changes === undefined)
77
+ return plan;
78
+ return {
79
+ ...plan,
80
+ detail: "summary",
81
+ changes: changes.map((c) => ({
82
+ file: c.file,
83
+ description: c.description,
84
+ beforeLines: c.before.split("\n").length,
85
+ afterLines: c.after.split("\n").length,
86
+ })),
87
+ };
88
+ }
89
+ const ROOT_FIELD = z.string().describe("Absolute path to the project root (containing BACKLOG.md/PROGRESS.md).");
90
+ // MCP-Annotations je Tool-Klasse (Step 9.2/M2): explizit gesetzt, auch wo der
91
+ // Spec-Default passen würde — Clients sollen nicht auf Defaults raten müssen.
92
+ export const READ_ONLY_ANNOTATIONS = {
93
+ readOnlyHint: true,
94
+ destructiveHint: false,
95
+ idempotentHint: true,
96
+ };
97
+ export const DESTRUCTIVE_ANNOTATIONS = {
98
+ readOnlyHint: false,
99
+ destructiveHint: true,
100
+ idempotentHint: false,
101
+ };
102
+ export const MUTATING_ANNOTATIONS = {
103
+ readOnlyHint: false,
104
+ destructiveHint: false,
105
+ idempotentHint: false,
106
+ };
107
+ const LOCALE_FIELD = z.enum(["de", "en"]).optional()
108
+ .describe("Language for generated texts (index line, done/verification markers). " +
109
+ "Default: auto-detection from the file context.");
110
+ function withoutRaw(item) {
111
+ const { raw: _raw, ...rest } = item;
112
+ return rest;
113
+ }
114
+ const PROJECTABLE_FIELDS = [
115
+ "id",
116
+ "title",
117
+ "priority",
118
+ "section",
119
+ "open",
120
+ "location",
121
+ "files",
122
+ "dependsOn",
123
+ "blockedBy",
124
+ "ready",
125
+ "text",
126
+ "span",
127
+ ];
128
+ function projectItem(item, fields) {
129
+ const full = withoutRaw(item);
130
+ return Object.fromEntries(fields.map((f) => [f, full[f]]));
131
+ }
132
+ export function registerDocsTools(server) {
133
+ server.registerTool("backlog_list", {
134
+ annotations: { ...READ_ONLY_ANNOTATIONS },
135
+ title: "Backlog list",
136
+ description: "Lists items of a STEPWELL project's BACKLOG.md (without raw, lean payload) " +
137
+ "including parse warnings; filter: priority, open, section, ready (M21/30.6: " +
138
+ "startable = open with no open Depends-on ancestors); projection: fields " +
139
+ "(e.g. [\"id\",\"title\",\"priority\",\"open\",\"section\"] for overview calls).",
140
+ inputSchema: {
141
+ root: ROOT_FIELD,
142
+ priority: z.array(z.enum(PRIORITY_VALUES)).optional()
143
+ .describe("Filter on priorities."),
144
+ open: z.boolean().optional().describe("Filter on open/completed checkbox."),
145
+ section: z.string().optional().describe("Exact section title (without emoji)."),
146
+ ready: z.boolean().optional()
147
+ .describe("M21/30.6: filter on startability (open AND no open Depends-on ancestors); items without a Depends-on bullet are ready when open."),
148
+ fields: z.array(z.enum(PROJECTABLE_FIELDS)).optional()
149
+ .describe("Project each item to the listed fields (token economy, E1); " +
150
+ "if the parameter is missing, all fields except raw are returned."),
151
+ },
152
+ }, ({ root, priority, open, section, ready, fields }) => asResult(() => {
153
+ const parsed = readBacklog(root);
154
+ let items = parsed.value.items;
155
+ if (priority !== undefined) {
156
+ const wanted = new Set(priority);
157
+ items = items.filter((i) => wanted.has(i.priority));
158
+ }
159
+ if (open !== undefined) {
160
+ items = items.filter((i) => i.open === open);
161
+ }
162
+ if (section !== undefined) {
163
+ items = items.filter((i) => i.section === section);
164
+ }
165
+ if (ready !== undefined) {
166
+ items = items.filter((i) => i.ready === ready);
167
+ }
168
+ return {
169
+ count: items.length,
170
+ items: items.map((i) => (fields ? projectItem(i, fields) : withoutRaw(i))),
171
+ warnings: parsed.warnings,
172
+ };
173
+ }));
174
+ server.registerTool("backlog_show", {
175
+ annotations: { ...READ_ONLY_ANNOTATIONS },
176
+ title: "Backlog item view",
177
+ description: "Merge view for an item id across the open BACKLOG, Done Index and BACKLOG_ARCHIVE " +
178
+ "— including raw + span; if the item is not identified, isError is returned.",
179
+ inputSchema: {
180
+ root: ROOT_FIELD,
181
+ id: z.string().describe("Item id, exact and case-sensitive (e.g. \"H1\")."),
182
+ },
183
+ }, ({ root, id }) => asResult(() => {
184
+ const entry = backlogShow(root, id);
185
+ if (entry === undefined) {
186
+ throw new Error(`unknown item id: ${id} (not in the open BACKLOG, Done Index or archive)`);
187
+ }
188
+ return entry;
189
+ }));
190
+ const STATUS_VALUES = ["⬜", "🔄", "✅", "⛔", "unknown"];
191
+ server.registerTool("progress_list", {
192
+ annotations: { ...READ_ONLY_ANNOTATIONS },
193
+ title: "Progress list",
194
+ description: "Lists the rows of the progress table in PROGRESS.md including parse warnings; " +
195
+ "filter: status (icon or \"unknown\").",
196
+ inputSchema: {
197
+ root: ROOT_FIELD,
198
+ status: z.enum(STATUS_VALUES).optional().describe("Filter on status icon."),
199
+ },
200
+ }, ({ root, status }) => asResult(() => {
201
+ const parsed = readProgress(root);
202
+ let rows = parsed.value.rows;
203
+ if (status !== undefined) {
204
+ rows = rows.filter((r) => r.status === status);
205
+ }
206
+ return { count: rows.length, rows, warnings: parsed.warnings };
207
+ }));
208
+ const matchesPhase = (block, phase) => block.name === phase ||
209
+ block.title === phase ||
210
+ `${block.name} — ${block.title}` === phase;
211
+ server.registerTool("progress_show", {
212
+ annotations: { ...READ_ONLY_ANNOTATIONS },
213
+ title: "Phase detail",
214
+ description: "Detail block of a phase (including raw) — search across active phases and archive; " +
215
+ "matched by name (\"Phase 2\"), title, or both (\"Phase 2 — UI Polish\").",
216
+ inputSchema: {
217
+ root: ROOT_FIELD,
218
+ phase: z.string().describe("Phase name or title, e.g. \"Phase 2\"."),
219
+ },
220
+ }, ({ root, phase }) => asResult(() => {
221
+ const docs = loadProject(root);
222
+ const block = docs.progress().value.phases.find((p) => matchesPhase(p, phase)) ??
223
+ docs.progressArchive().value.find((p) => matchesPhase(p, phase));
224
+ if (block === undefined) {
225
+ throw new Error(`unknown phase: ${phase} (neither a running phase nor in the archive)`);
226
+ }
227
+ return block;
228
+ }));
229
+ server.registerTool("docs_status", {
230
+ annotations: { ...READ_ONLY_ANNOTATIONS },
231
+ title: "Docs status",
232
+ description: "Aggregate of a STEPWELL project: open items per priority, running steps/phases, " +
233
+ "✅-ratio and all validation/parse warnings.",
234
+ inputSchema: {
235
+ root: ROOT_FIELD,
236
+ include: z.array(z.enum(["nextStepScope"])).optional()
237
+ .describe('Additional context: "nextStepScope" returns the goal/acceptance/scope bullet + ' +
238
+ "merged backlog item of the next step (1 call instead of 3, E2/12.3)."),
239
+ },
240
+ }, ({ root, include }) => asResult(() => docsStatus(root, include ? { include } : undefined)));
241
+ server.registerTool("docs_validate", {
242
+ annotations: { ...READ_ONLY_ANNOTATIONS },
243
+ title: "Docs validation",
244
+ description: "Checks cross-consistency of the four doc files (Done Index ↔ archive, checkbox ↔ " +
245
+ "archival, 🔄 ↔ detail block, id/date convention) and collects parse warnings.",
246
+ inputSchema: { root: ROOT_FIELD, structured: STRUCTURED_FIELD },
247
+ }, ({ root, structured }) => asStructuredResult(() => docsValidate(root), structured));
248
+ server.registerTool("docs_repair", {
249
+ annotations: { ...DESTRUCTIVE_ANNOTATIONS },
250
+ title: "Docs repair (dry-run)",
251
+ description: "Mechanical repair path for validate findings, over the responsible planners — never " +
252
+ "text manipulation: EMPTY_MARKER_STALE (placeholder removal), DATE_LEGACY YYMMDD/HHMM " +
253
+ "(→ ISO 8601; MM/YYYY stays a finding — the day is not reconstructible), NOT_ARCHIVED " +
254
+ "(delegates to archive_item), ARCHIVE_WITHOUT_INDEX (Done-Index line from the archive " +
255
+ "block title), WIP_WITHOUT_PLAN (skeleton, only when the phase has no other table rows). " +
256
+ "Everything else stays a finding (human judgment). Applies sequentially with " +
257
+ "re-validation between repairs; refusals are reported, never retried. Deliberately a " +
258
+ "SEPARATE tool from docs_validate: a tool cannot be read-only AND mutating — the " +
259
+ "static readOnly annotation of docs_validate protects consumer auto-approval flows (M4).",
260
+ inputSchema: {
261
+ root: ROOT_FIELD,
262
+ structured: STRUCTURED_FIELD,
263
+ dryRun: z.boolean().default(true)
264
+ .describe("true (default): report planned/skipped without writing; false: apply the mechanical repairs."),
265
+ },
266
+ }, ({ root, structured, dryRun }) => asStructuredResult(() => docsRepair(root, { dryRun }), structured));
267
+ server.registerTool("archive_item", {
268
+ annotations: { ...DESTRUCTIVE_ANNOTATIONS },
269
+ title: "Archive item (dry-run)",
270
+ description: "Plans the verbatim move of a completed backlog item into BACKLOG_ARCHIVE " +
271
+ "(checkbox → [x], optional note as **Done:** line) plus a one-liner in the " +
272
+ "Done Index. Returns the plan with a diff preview; nothing is written unless " +
273
+ "dryRun is false.",
274
+ inputSchema: {
275
+ root: ROOT_FIELD,
276
+ id: z.string().describe("Item id of the open backlog item, exact (e.g. \"H1\")."),
277
+ note: z.string().optional()
278
+ .describe("Optional done note (e.g. commit hash) — ends up in the archive block and index tail."),
279
+ locale: LOCALE_FIELD,
280
+ structured: STRUCTURED_FIELD,
281
+ detail: DETAIL_FIELD,
282
+ dryRun: z.boolean().default(true)
283
+ .describe("true (default): plan/diff preview only; false: write the changes."),
284
+ commit: COMMIT_FIELD,
285
+ commitMessage: COMMIT_MESSAGE_FIELD,
286
+ },
287
+ }, ({ root, id, note, locale, structured, detail, dryRun, commit, commitMessage }) => asStructuredResult(() => {
288
+ const plan = planArchiveItem(root, id, {
289
+ dryRun,
290
+ ...(note !== undefined ? { note } : {}),
291
+ ...(locale !== undefined ? { locale: locale } : {}),
292
+ });
293
+ const result = plan.dryRun ? plan : applyArchivePlan(plan);
294
+ if (!plan.dryRun && commit) {
295
+ attachCommit(result, root, commitMessage ?? docCommitMessage({ kind: "archive", id }));
296
+ }
297
+ return detail === "summary" ? summarizePlan(result) : result;
298
+ }, structured));
299
+ server.registerTool("progress_update", {
300
+ annotations: { ...MUTATING_ANNOTATIONS },
301
+ title: "Update step status (dry-run)",
302
+ description: "Sets the status of a step in the progress table (missing rows are added), " +
303
+ "creates a detail block skeleton under 'Active Phases' on 🔄 and moves complete " +
304
+ "phases verbatim into PROGRESS_ARCHIVE (note → **Verification:** line). Returns the " +
305
+ "plan with a diff preview; nothing is written unless dryRun is false.",
306
+ inputSchema: {
307
+ root: ROOT_FIELD,
308
+ phase: z.string().describe("Phase name, title, both, or a bare phase number (e.g. \"Phase 2\" / \"Phase 2 — UI Polish\" / \"2\")."),
309
+ step: z.union([z.string(), z.array(z.string())])
310
+ .describe("Step number per table/scope (e.g. \"2.2\") or an array of step numbers for multi-step updates in one call (e.g. phase completion; only with dryRun: false)."),
311
+ status: z.enum(["⬜", "🔄", "✅", "⛔"]).describe("New status icon."),
312
+ title: z.string().optional()
313
+ .describe("New phase title — renames the detail block heading consistently (table stays unchanged); a full heading text is accepted as-is, no prefix doubling (L16/29.3); on phase completion in the same call the block moves into the archive under the new title."),
314
+ note: z.string().optional()
315
+ .describe("Optional note — on phase completion it becomes the **Verification:** line on the archive block."),
316
+ checkpoint: z.string().optional()
317
+ .describe("Optional commit SHA of the phase (7–40 hex) — on phase completion it appears in the verification line of the archive block."),
318
+ locale: LOCALE_FIELD,
319
+ structured: STRUCTURED_FIELD,
320
+ detail: DETAIL_FIELD,
321
+ dryRun: z.boolean().default(true)
322
+ .describe("true (default): plan/diff preview only; false: write the changes."),
323
+ commit: COMMIT_FIELD,
324
+ commitMessage: COMMIT_MESSAGE_FIELD,
325
+ },
326
+ }, ({ root, phase, step, status, title, note, checkpoint, locale, structured, detail, dryRun, commit, commitMessage }) => asStructuredResult(() => {
327
+ const steps = Array.isArray(step) ? step : [step];
328
+ if (steps.length > 1 && dryRun) {
329
+ throw new Error(`multi-step (${steps.join(", ")}) requires dryRun: false — dry-run previews are only defined for a single step`);
330
+ }
331
+ if (steps.length > 1 && detail === "summary") {
332
+ throw new Error(`multi-step (${steps.join(", ")}) returns the aggregate result — detail: "summary" is only defined for a single step`);
333
+ }
334
+ const planOptions = {
335
+ dryRun,
336
+ ...(title !== undefined ? { title } : {}),
337
+ ...(note !== undefined ? { note } : {}),
338
+ ...(checkpoint !== undefined ? { checkpoint } : {}),
339
+ ...(locale !== undefined ? { locale: locale } : {}),
340
+ };
341
+ const archivedThisBatch = new Set();
342
+ const runSingle = (oneStep) => {
343
+ const plan = planProgressUpdate(root, phase, oneStep, status, {
344
+ ...planOptions,
345
+ ...(archivedThisBatch.has(phase) ? { phaseAlreadyArchived: true } : {}),
346
+ });
347
+ if (plan.completedPhase)
348
+ archivedThisBatch.add(phase);
349
+ const result = plan.dryRun ? plan : applyProgressPlan(plan);
350
+ return detail === "summary" ? summarizePlan(result) : result;
351
+ };
352
+ if (steps.length === 1) {
353
+ const result = runSingle(steps[0]);
354
+ if (!dryRun && commit) {
355
+ attachCommit(result, root, commitMessage ?? docCommitMessage({ kind: "progress", steps, status: status }));
356
+ }
357
+ return result;
358
+ }
359
+ // M9/18.1: validate every step up-front (planning is read-only) so a bad
360
+ // step fails before the first write; residual apply-time failures report
361
+ // what was already persisted.
362
+ steps.map((oneStep) => planProgressUpdate(root, phase, oneStep, status, planOptions));
363
+ const writtenSoFar = [];
364
+ try {
365
+ const results = steps.map((oneStep) => {
366
+ const result = runSingle(oneStep);
367
+ writtenSoFar.push(...result.written);
368
+ return result;
369
+ });
370
+ const written = [...new Set(results.flatMap((r) => r.written))];
371
+ const messages = results.flatMap((r) => r.verification.messages);
372
+ const allOk = results.every((r) => r.verification.ok);
373
+ const aggregate = { written, steps, verification: { ok: allOk, messages } };
374
+ // one doc commit for the whole batch (H5/26.3), only after a fully successful apply
375
+ if (commit) {
376
+ attachCommit(aggregate, root, commitMessage ?? docCommitMessage({ kind: "progress", steps, status: status }));
377
+ }
378
+ return aggregate;
379
+ }
380
+ catch (error) {
381
+ const message = error instanceof Error ? error.message : String(error);
382
+ throw new Error(`multi-step (${steps.join(", ")}) failed after partially applying — ` +
383
+ `already written: ${writtenSoFar.length > 0 ? writtenSoFar.join(", ") : "(none)"} — ` +
384
+ `cause: ${message}`);
385
+ }
386
+ }, structured));
387
+ server.registerTool("backlog_add", {
388
+ annotations: { ...MUTATING_ANNOTATIONS },
389
+ title: "Create backlog item (dry-run)",
390
+ description: "Creates an open backlog item at the end of the target section: conforming id " +
391
+ "(series convention, auto-assigned K/H/M/L per priority or explicit), correct " +
392
+ "block format, As of: timestamp updated. Returns the plan with a diff preview; " +
393
+ "nothing is written unless dryRun is false.",
394
+ inputSchema: {
395
+ root: ROOT_FIELD,
396
+ section: z.string().describe("Target section (title without emoji, e.g. \"HIGH\")."),
397
+ title: z.string().describe("Item title after the id."),
398
+ priority: z.enum(PRIORITY_WRITE_VALUES).describe("Priority emoji (part of the title)."),
399
+ id: z.string().optional()
400
+ .describe("Explicit item id (convention ^[A-Z][0-9]+$); if missing, the next free number of the priority series (K/H/M/L) is assigned. 🔵 requires an explicit id."),
401
+ text: TEXT_FIELD,
402
+ detail: DETAIL_FIELD,
403
+ dryRun: DRYRUN_FIELD,
404
+ considerWorktrees: z.boolean().default(false)
405
+ .describe("M20/30.5: when true, the next free id also skips the ids occupied in ALL foreign worktree doc states (git satellite read); without git it falls back to the own tree with a worktreeWarnings entry. Explicit ids are never filtered."),
406
+ },
407
+ }, ({ root, section, title, priority, id, text, detail, dryRun, considerWorktrees }) => asResult(() => {
408
+ const worktreeWarnings = [];
409
+ let excludeIds;
410
+ if (considerWorktrees) {
411
+ const foreign = foreignDocState(root);
412
+ excludeIds = foreign.ids;
413
+ worktreeWarnings.push(...foreign.warnings);
414
+ }
415
+ const plan = planBacklogAdd(root, {
416
+ dryRun,
417
+ section,
418
+ title,
419
+ priority: priority,
420
+ ...(id !== undefined ? { id } : {}),
421
+ ...(text !== undefined ? { text } : {}),
422
+ ...(excludeIds !== undefined ? { excludeIds } : {}),
423
+ });
424
+ const result = plan.dryRun ? plan : applyBacklogAddPlan(plan);
425
+ const withWarnings = worktreeWarnings.length > 0 ? { ...result, worktreeWarnings } : result;
426
+ return detail === "summary" ? summarizePlan(withWarnings) : withWarnings;
427
+ }));
428
+ server.registerTool("backlog_update", {
429
+ annotations: { ...MUTATING_ANNOTATIONS },
430
+ title: "Update backlog item (dry-run)",
431
+ description: "Changes title, priority, text or section of an open item in the block format " +
432
+ "(span re-computation, As of: timestamp). A priority change moves the block to the " +
433
+ "matching priority section; `position` (L17/30.7) places the block within its " +
434
+ "(target) section — top/bottom/1-based slot, byte-identical content, structured " +
435
+ "out-of-range refusal. Returns the plan with a diff preview; nothing is " +
436
+ "written unless dryRun is false.",
437
+ inputSchema: {
438
+ root: ROOT_FIELD,
439
+ id: z.string().describe("Item id of the open backlog item, exact (e.g. \"H1\")."),
440
+ title: z.string().optional().describe("New title."),
441
+ priority: z.enum(PRIORITY_WRITE_VALUES).optional().describe("New priority."),
442
+ section: z.string().optional()
443
+ .describe("New target section (title without emoji); default: matching priority section on priority change, otherwise the item stays in place."),
444
+ position: z.union([z.literal("top"), z.literal("bottom"), z.number().int().min(1)]).optional()
445
+ .describe("L17/30.7: block position within the (target) section — \"top\", \"bottom\" or a 1-based slot among the section's item blocks; with `section` the order is first switch, then place; out of range is a structured error."),
446
+ text: TEXT_FIELD,
447
+ detail: DETAIL_FIELD,
448
+ dryRun: DRYRUN_FIELD,
449
+ },
450
+ }, ({ root, id, title, priority, section, position, text, detail, dryRun }) => asResult(() => {
451
+ const plan = planBacklogUpdate(root, id, {
452
+ dryRun,
453
+ ...(title !== undefined ? { title } : {}),
454
+ ...(priority !== undefined ? { priority: priority } : {}),
455
+ ...(section !== undefined ? { section } : {}),
456
+ ...(position !== undefined ? { position } : {}),
457
+ ...(text !== undefined ? { text } : {}),
458
+ });
459
+ const result = plan.dryRun ? plan : applyBacklogUpdatePlan(plan);
460
+ return detail === "summary" ? summarizePlan(result) : result;
461
+ }));
462
+ server.registerTool("backlog_remove", {
463
+ annotations: { ...DESTRUCTIVE_ANNOTATIONS },
464
+ title: "Remove backlog item (dry-run)",
465
+ description: "Removes an open item WITHOUT hard delete: the block moves verbatim into BACKLOG_ARCHIVE " +
466
+ "(checkbox stays [ ], optional note as **Removed:** line), the Done Index gets a tail " +
467
+ "without a Done marker. Returns the plan with a diff preview; nothing is written " +
468
+ "unless dryRun is false.",
469
+ inputSchema: {
470
+ root: ROOT_FIELD,
471
+ id: z.string().describe("Item id of the open backlog item, exact (e.g. \"H1\")."),
472
+ note: z.string().optional()
473
+ .describe("Optional removal note — ends up in the archive block and index tail."),
474
+ locale: LOCALE_FIELD,
475
+ detail: DETAIL_FIELD,
476
+ dryRun: DRYRUN_FIELD,
477
+ },
478
+ }, ({ root, id, note, locale, detail, dryRun }) => asResult(() => {
479
+ const plan = planBacklogRemove(root, id, {
480
+ dryRun,
481
+ ...(note !== undefined ? { note } : {}),
482
+ ...(locale !== undefined ? { locale: locale } : {}),
483
+ });
484
+ const result = plan.dryRun ? plan : applyBacklogRemovePlan(plan);
485
+ return detail === "summary" ? summarizePlan(result) : result;
486
+ }));
487
+ server.registerTool("progress_plan_phase", {
488
+ annotations: { ...MUTATING_ANNOTATIONS },
489
+ title: "Plan phase ahead (dry-run)",
490
+ description: "Plans a new phase: creates table rows for all steps (⬜, with names) and a " +
491
+ "detail block skeleton with the full scope under 'Active Phases'. If the phase " +
492
+ "already exists as an active phase, it is extended with the new steps (missing " +
493
+ "table rows + scope bullets, without touching neighbouring rows). Validates that " +
494
+ "the step numbers match the phase name (Phase <N> → <N>.<x>) and that phase/steps " +
495
+ "are still free. Returns the plan with a diff preview; nothing is written unless " +
496
+ "dryRun is false.",
497
+ inputSchema: {
498
+ root: ROOT_FIELD,
499
+ phase: z.string().describe('New phase in the pattern "Phase <N>[ — Title]", e.g. "Phase 7 — Polish".'),
500
+ steps: z
501
+ .array(z.object({ step: z.string(), name: z.string() }))
502
+ .describe("Steps of the phase in order (e.g. [{ step: \"7.1\", name: \"Setup\" }])."),
503
+ detail: DETAIL_FIELD,
504
+ dryRun: DRYRUN_FIELD,
505
+ considerWorktrees: z.boolean().default(false)
506
+ .describe("M20/30.5: when true, the phase number and steps must also be free in ALL foreign worktree doc states (structured refusal otherwise); without git it falls back to the own tree with a worktreeWarnings entry."),
507
+ },
508
+ }, ({ root, phase, steps, detail, dryRun, considerWorktrees }) => asResult(() => {
509
+ const worktreeWarnings = [];
510
+ let excludePhases;
511
+ let excludeSteps;
512
+ if (considerWorktrees) {
513
+ const foreign = foreignDocState(root);
514
+ excludePhases = foreign.phases;
515
+ excludeSteps = foreign.steps;
516
+ worktreeWarnings.push(...foreign.warnings);
517
+ }
518
+ const plan = planPhase(root, phase, steps, {
519
+ dryRun,
520
+ ...(excludePhases !== undefined ? { excludePhases } : {}),
521
+ ...(excludeSteps !== undefined ? { excludeSteps } : {}),
522
+ });
523
+ const result = plan.dryRun ? plan : applyPhasePlan(plan);
524
+ const withWarnings = worktreeWarnings.length > 0 ? { ...result, worktreeWarnings } : result;
525
+ return detail === "summary" ? summarizePlan(withWarnings) : withWarnings;
526
+ }));
527
+ server.registerTool("workstream_sync", {
528
+ annotations: { ...MUTATING_ANNOTATIONS },
529
+ title: "Workstream sync (dry-run)",
530
+ description: "Git workstream sync (Decision 14/H5): pull merges the trunk into this worktree branch " +
531
+ "(the risky side — conflicts surface here as a clean abort with a structured report), push " +
532
+ "fast-forwards the trunk to this branch (precondition: trunk head already pulled — never a " +
533
+ "real merge on the trunk side), status returns the read-only workstream report (per-worktree " +
534
+ "liveness + conflict-candidate overlap, derived from git itself). Dry-run (default) runs the " +
535
+ "exact guard chain of the real operation and previews the effect without touching anything.",
536
+ inputSchema: {
537
+ root: ROOT_FIELD,
538
+ direction: z.enum(["pull", "push", "status"]).describe("pull: merge trunk → branch. push: fast-forward trunk → branch head. status: read-only report."),
539
+ structured: STRUCTURED_FIELD,
540
+ dryRun: z.boolean().default(true)
541
+ .describe("true (default): pre-flight — guard chain + preview, no git mutation. false: execute pull/push. Ignored for status."),
542
+ },
543
+ }, ({ root, direction, structured, dryRun }) => asStructuredResult(() => {
544
+ if (direction === "status")
545
+ return workstreamReport(root);
546
+ const preview = syncPreview(root, direction);
547
+ if (dryRun || !preview.ok)
548
+ return preview;
549
+ return direction === "pull" ? pullTrunk(root) : pushTrunk(root);
550
+ }, structured));
551
+ }
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "stepwell",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.4",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist",
8
+ "skills"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "bin": {
17
+ "stepwell": "./dist/cli.js"
18
+ },
19
+ "dependencies": {
20
+ "stepwell-core": "*",
21
+ "stepwell-git": "*",
22
+ "@modelcontextprotocol/sdk": "^1.30.0",
23
+ "zod": "^3.25.0"
24
+ },
25
+ "scripts": {
26
+ "typecheck": "tsc -p tsconfig.json",
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "prepublishOnly": "npm run build"
29
+ }
30
+ }