archgen-mcp 0.1.1 → 0.1.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.
@@ -11,6 +11,7 @@ import { loadAsset } from '../context.mjs';
11
11
  import { setTaskStatus, STATUSES } from '../core/status-mutator.mjs';
12
12
  import { writeResumeSnapshot } from '../core/resume-snapshot.mjs';
13
13
  import { invalidInput } from '../core/errors.mjs';
14
+ import { moreMeta, project, withMeta } from '../envelope.mjs';
14
15
  import { resolveSlugInput, ScopeError } from '../core/scope.mjs';
15
16
  import { dirname, join } from 'node:path';
16
17
 
@@ -40,13 +41,15 @@ const ACTION_DESC = 'next | set_status | list | snapshot | contract — see arch
40
41
  const schema = z.object({
41
42
  action: z.enum(['next', 'set_status', 'list', 'snapshot', 'contract']).describe(ACTION_DESC),
42
43
  tasksYaml: z.string().optional().describe('Absolute path to tasks.yaml. Required for next, set_status and list.'),
43
- taskId: z.string().optional().describe('Task id (set_status).'),
44
+ taskId: z.string().optional().describe('Task id (set_status, contract). ONE task (list, next).'),
44
45
  status: z.enum(STATUSES).optional().describe('New status (set_status).'),
45
46
  force: z.boolean().optional().describe('Allow reopening a done task (set_status).'),
46
47
  filterStatus: z.string().optional().describe('Filter by status (list).'),
47
48
  slugDir: z.string().optional().describe('The .archgen/<slug>/ directory (contract) — locates the plan docs.'),
48
49
  codeStandardsPath: z.string().optional().describe('Path to the code-standards reference (contract).'),
49
50
  module: z.string().optional().describe('Filter by module path fragment (list).'),
51
+ limit: z.number().int().positive().optional().describe('Max items to return. Default 50.'),
52
+ offset: z.number().int().nonnegative().optional().describe('Skip this many items.'),
50
53
  projectRoot: projectRootArg,
51
54
  });
52
55
 
@@ -74,10 +77,37 @@ export function registerTaskTools(server) {
74
77
  }
75
78
  case 'list': {
76
79
  if (!args.tasksYaml) throw invalidInput('tasksYaml is required for list', { action });
77
- return listTasks(ctx.contain(args.tasksYaml), {
80
+ const listed = listTasks(ctx.contain(args.tasksYaml), {
78
81
  status: args.filterStatus ?? null,
79
82
  module: args.module ?? null,
80
83
  });
84
+ // ONE task, when the caller named one.
85
+ if (args.taskId) {
86
+ const one = (listed.tasks ?? []).find((t) => t.id === args.taskId);
87
+ if (!one) {
88
+ throw invalidInput(`no task '${args.taskId}' in ${args.tasksYaml}`, {
89
+ action,
90
+ taskId: args.taskId,
91
+ available: (listed.tasks ?? []).map((t) => t.id),
92
+ });
93
+ }
94
+ return { task: one, tasks: [one] };
95
+ }
96
+ const p = project(listed.tasks ?? [], {
97
+ limit: args.limit,
98
+ offset: args.offset,
99
+ next: `archgen_tasks{action:'list',tasksYaml:'${args.tasksYaml}',limit:${args.limit ?? 50},offset:${(args.offset ?? 0) + (args.limit ?? 50)}}`,
100
+ });
101
+ return withMeta(
102
+ { ...listed, tasks: p.items },
103
+ moreMeta({
104
+ total: p.total,
105
+ returned: p.returned,
106
+ next: p.next,
107
+ available: { tasks: p.total },
108
+ more: `archgen_tasks{action:'list',tasksYaml:'${args.tasksYaml}',taskId:'<one task>'}`,
109
+ }),
110
+ );
81
111
  }
82
112
  case 'snapshot': {
83
113
  if (!args.tasksYaml) throw invalidInput('tasksYaml is required for snapshot', { action });
@@ -196,7 +226,42 @@ export function registerTaskTools(server) {
196
226
  }
197
227
  case 'next': {
198
228
  if (!args.tasksYaml) throw invalidInput('tasksYaml is required for next', { action });
199
- return resolveWaves(ctx.contain(args.tasksYaml));
229
+ const resolved = resolveWaves(ctx.contain(args.tasksYaml));
230
+ // ONE wave, when the caller named a task: "which wave is this in?"
231
+ if (args.taskId) {
232
+ const index = resolved.waves.findIndex((w) => w.some((t) => t.id === args.taskId));
233
+ if (index < 0) {
234
+ throw invalidInput(`no task '${args.taskId}' in ${args.tasksYaml}`, {
235
+ action,
236
+ taskId: args.taskId,
237
+ available: resolved.waves.flat().map((t) => t.id),
238
+ });
239
+ }
240
+ return {
241
+ waveIndex: index,
242
+ wave: resolved.waves[index],
243
+ waves: [resolved.waves[index]],
244
+ blockedByFailure: resolved.blockedByFailure,
245
+ };
246
+ }
247
+ // The waves are the plan's shape; a long plan would dump every task
248
+ // in every wave, so the wave COUNT is reported and the waves
249
+ // themselves are bounded.
250
+ const p = project(resolved.waves, {
251
+ limit: args.limit,
252
+ offset: args.offset,
253
+ next: `archgen_tasks{action:'next',tasksYaml:'${args.tasksYaml}',limit:${args.limit ?? 50},offset:${(args.offset ?? 0) + (args.limit ?? 50)}}`,
254
+ });
255
+ return withMeta(
256
+ { ...resolved, waves: p.items },
257
+ moreMeta({
258
+ total: p.total,
259
+ returned: p.returned,
260
+ next: p.next,
261
+ available: { waves: p.total, tasks: resolved.waves.flat().length },
262
+ more: `archgen_tasks{action:'next',tasksYaml:'${args.tasksYaml}',taskId:'<one task>'}`,
263
+ }),
264
+ );
200
265
  }
201
266
  default:
202
267
  throw invalidInput(`unknown action '${action}'`, { action });