archgen-mcp 0.1.0 → 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.
@@ -8,6 +8,7 @@ import { z } from 'zod/v4';
8
8
  import { registerTool, projectRootArg } from './_register.mjs';
9
9
  import { loadAsset } from '../context.mjs';
10
10
  import { manageCodegraph, assertLocation } from '../core/codegraph-manager.mjs';
11
+ import { moreMeta, project, withMeta } from '../envelope.mjs';
11
12
 
12
13
  const ACTION_DESC = 'ensure | status | wire — see archgen_help.';
13
14
 
@@ -17,6 +18,8 @@ const schema = z.object({
17
18
  approve: z.boolean().optional().describe('Required to write any MCP config. Without it you get the plan and nothing is written.'),
18
19
  telemetry: z.enum(['keep', 'off', 'on']).optional().describe('Telemetry preference. Defaults to keep (leave as-is).'),
19
20
  noInstall: z.boolean().optional().describe('Do not install the CLI; use the npx fallback in configs.'),
21
+ limit: z.number().int().positive().optional().describe('Max items to return. Default 50.'),
22
+ offset: z.number().int().nonnegative().optional().describe('Skip this many items.'),
20
23
  projectRoot: projectRootArg,
21
24
  });
22
25
 
@@ -37,7 +40,7 @@ export function registerCodegraphTools(server) {
37
40
  const registry = loadAsset('harness-mcp.json');
38
41
  const location = assertLocation(args.location ?? 'both');
39
42
  const mode = args.action === 'status' ? 'status' : args.action === 'wire' ? 'wire' : 'ensure';
40
- return manageCodegraph({
43
+ const report = await manageCodegraph({
41
44
  mode,
42
45
  projectRoot: ctx.projectRoot,
43
46
  registry,
@@ -48,6 +51,22 @@ export function registerCodegraphTools(server) {
48
51
  home: ctx.home,
49
52
  env: ctx.env,
50
53
  });
54
+ // The wiring lists grow with the number of detected harnesses, so they
55
+ // are bounded — and the agent is told what else it could fetch.
56
+ const p = project(report.detected ?? [], {
57
+ limit: args.limit,
58
+ offset: args.offset,
59
+ next: `archgen_codegraph{action:'status',limit:${args.limit ?? 50},offset:${(args.offset ?? 0) + (args.limit ?? 50)}}`,
60
+ });
61
+ return withMeta(
62
+ { ...report, detected: p.items },
63
+ moreMeta({
64
+ total: p.total,
65
+ returned: p.returned,
66
+ next: p.next,
67
+ available: { harnesses: p.total },
68
+ }),
69
+ );
51
70
  },
52
71
  });
53
72
  }
@@ -6,6 +6,8 @@ import { join } from 'node:path';
6
6
  import { registerTool, projectRootArg } from './_register.mjs';
7
7
  import { docIndex } from '../core/doc-navigator.mjs';
8
8
  import { invalidInput } from '../core/errors.mjs';
9
+ import { hint, teach } from '../core/guidance.mjs';
10
+ import { moreMeta, project, withMeta } from '../envelope.mjs';
9
11
 
10
12
  const ACTION_DESC = 'index | digest | section | refs | map | validate — see archgen_help.';
11
13
 
@@ -18,6 +20,8 @@ const schema = z.object({
18
20
  token: z.string().optional().describe('The token to look for (refs, map).'),
19
21
  section: z.string().optional().describe('Section path, optionally path#anchor (section).'),
20
22
  file: z.string().optional().describe('Scope to one document (repo-root-relative or slug-relative).'),
23
+ limit: z.number().int().positive().optional().describe('Max items to return. Default 50.'),
24
+ offset: z.number().int().nonnegative().optional().describe('Skip this many items.'),
21
25
  projectRoot: projectRootArg,
22
26
  });
23
27
 
@@ -35,22 +39,46 @@ export function registerDocTools(server) {
35
39
  idempotent: true,
36
40
  handler: (ctx, args) => {
37
41
  const slugDir = resolveSlugDir(ctx, args.slugDir);
42
+ const bound = args.limit ?? 50;
43
+ const from = args.offset ?? 0;
38
44
  switch (args.action) {
39
- case 'index':
40
- return docIndex(slugDir, { mode: 'overview', file: args.file ?? null, projectRoot: ctx.projectRoot });
41
- case 'digest':
42
- return docIndex(slugDir, { mode: 'digest', file: args.file ?? null, projectRoot: ctx.projectRoot });
43
- case 'validate':
45
+ case 'index': {
46
+ const overview = docIndex(slugDir, { mode: 'overview', file: args.file ?? null, projectRoot: ctx.projectRoot });
47
+ return bounded('files', overview.data, args, `archgen_docs{action:'index',slugDir:'${slugDir}',limit:${bound},offset:${from + bound}}`, {
48
+ files: (overview.data.files ?? []).length,
49
+ more: `archgen_docs{action:'section',slugDir:'${slugDir}',section:'<the section you need>'}`,
50
+ });
51
+ }
52
+ case 'digest': {
53
+ const digest = docIndex(slugDir, { mode: 'digest', file: args.file ?? null, projectRoot: ctx.projectRoot });
54
+ return bounded('files', digest.data, args, `archgen_docs{action:'digest',slugDir:'${slugDir}',limit:${bound},offset:${from + bound}}`, {
55
+ files: (digest.data.files ?? []).length,
56
+ more: `archgen_docs{action:'section',slugDir:'${slugDir}',section:'<the section you need>'}`,
57
+ });
58
+ }
59
+ case 'validate': {
44
60
  return docIndex(slugDir, { mode: 'validate', file: args.file ?? null, projectRoot: ctx.projectRoot });
45
- case 'refs':
61
+ }
62
+ case 'refs': {
46
63
  if (!args.token) throw invalidInput('refs requires a token', { action: args.action });
47
- return docIndex(slugDir, { mode: 'refs', token: args.token, file: args.file ?? null, projectRoot: ctx.projectRoot });
48
- case 'map':
64
+ const refs = docIndex(slugDir, { mode: 'refs', token: args.token, file: args.file ?? null, projectRoot: ctx.projectRoot });
65
+ return bounded('matches', refs.data, args, `archgen_docs{action:'refs',slugDir:'${slugDir}',token:'${args.token}',limit:${bound},offset:${from + bound}}`, {
66
+ matches: (refs.data.matches ?? []).length,
67
+ });
68
+ }
69
+ case 'map': {
49
70
  if (!args.token) throw invalidInput('map requires a token', { action: args.action });
50
- return docIndex(slugDir, { mode: 'map', token: args.token, file: args.file ?? null, projectRoot: ctx.projectRoot });
51
- case 'section':
71
+ const map = docIndex(slugDir, { mode: 'map', token: args.token, file: args.file ?? null, projectRoot: ctx.projectRoot });
72
+ return bounded('sections', map.data, args, `archgen_docs{action:'map',slugDir:'${slugDir}',token:'${args.token}',limit:${bound},offset:${from + bound}}`, {
73
+ sections: (map.data.sections ?? []).length,
74
+ matches: map.data.matchCount ?? undefined,
75
+ more: `archgen_docs{action:'section',slugDir:'${slugDir}',section:'<the section you need>'}`,
76
+ });
77
+ }
78
+ case 'section': {
52
79
  if (!args.section) throw invalidInput('section requires a section path', { action: args.action });
53
80
  return docIndex(slugDir, { mode: 'section', section: args.section, file: args.file ?? null, projectRoot: ctx.projectRoot });
81
+ }
54
82
  default:
55
83
  throw invalidInput(`unknown action '${args.action}'`, { action: args.action });
56
84
  }
@@ -58,6 +86,24 @@ export function registerDocTools(server) {
58
86
  });
59
87
  }
60
88
 
89
+ /**
90
+ * Bound a collection-shaped answer, and report what was left.
91
+ *
92
+ * The agent is told how many items exist, how many it got, and the exact
93
+ * call that returns the next slice — so a bound never hides data.
94
+ *
95
+ * @param {string} key the payload key the collection sits in
96
+ * @param {object} data the payload
97
+ * @param {any} args
98
+ * @param {string} next the exact next call
99
+ * @param {object} available what else exists
100
+ */
101
+ function bounded(key, data, args, next, available) {
102
+ const items = data?.[key] ?? [];
103
+ const p = project(items, { limit: args.limit, offset: args.offset, next });
104
+ return withMeta({ ...data, [key]: p.items }, moreMeta({ total: p.total, returned: p.returned, next: p.next, available }));
105
+ }
106
+
61
107
  /**
62
108
  * Resolve the slug directory: explicit argument, else the most recently
63
109
  * modified `.archgen/<slug>/` under the project root.
@@ -69,8 +115,15 @@ export function resolveSlugDir(ctx, explicit) {
69
115
  const active = activeSlugDir(ctx);
70
116
  if (!active) {
71
117
  throw invalidInput(
72
- `no .archgen/<slug>/ directory found under ${ctx.projectRoot} — generate a plan first, or pass slugDir`,
73
- { projectRoot: ctx.projectRoot },
118
+ teach(
119
+ `no .archgen/<slug>/ directory under ${ctx.projectRoot}`,
120
+ 'scaffold returns file CONTENT and writes nothing, so write it first',
121
+ "archgen_plan{action:'scaffold',scope:'LARGE',slug:'<slug>'}",
122
+ ),
123
+ hint({
124
+ action: "archgen_plan{action:'scaffold',scope:'LARGE',slug:'<slug>'}",
125
+ reason: 'scaffold returns content; you write it.',
126
+ }),
74
127
  );
75
128
  }
76
129
  return active;
@@ -43,9 +43,12 @@ Validates and analyzes a plan (tasks.yaml + plans/).
43
43
  - \`status\` only applies to mermaid output.
44
44
  - \`module\` cannot be combined with \`node\` or mermaid.
45
45
  - Whole-graph mermaid is refused above 250 nodes — scope it with \`node\` or \`module\`.
46
- - \`scaffold\` returns the artifact set a scope class MUST have — adding or dropping files is a right-sizing defect.
46
+ - \`scaffold\` returns the artifact set a scope class MUST have — adding or dropping files is a right-sizing defect. It returns each file's CONTENT and **writes nothing**: you write them under \`.archgen/<slug>/\`, then the artifact tools can read them.
47
47
  - \`brief\` returns a ready-to-send sub-agent prompt (verifier | review | survey | investigate). It derives the slug from \`tasksYaml\` when you do not pass one, and every path in it is ABSOLUTE — so it works from any working directory. The verifier and review briefs carry the graph, the reference-integrity result and the evidence map rendered, so the sub-agent makes no tool call of its own.
48
- - The verifier and reviewer return \`VERDICT:\` plus a COVERAGE table (every dimension PASS / FAIL / N-A). A report with findings but no coverage table is INCOMPLETE.`,
48
+ - The verifier and reviewer return \`VERDICT:\` plus a COVERAGE table (every dimension PASS / FAIL / N-A). A report with findings but no coverage table is INCOMPLETE.
49
+ - **Every error carries the next call.** If one fails, read the \`Next:\` line in the message — it is the exact call to make, and the \`Why:\` clause says why. Do not retry the same call.
50
+ - **Batch calls with \`allSettled\`**, not \`all\`: one bad argument must not discard the results of its siblings.
51
+ - **Narrow before you fetch.** \`coverage\` takes \`taskId\` for one task; \`scaffold\` takes \`file\` for one skeleton; \`limit\`/\`offset\` bound the rest. The whole set is never the default answer.`,
49
52
 
50
53
  tasks: `# archgen_tasks
51
54
 
@@ -80,7 +83,8 @@ Dispatching the next wave while any of those lag is forbidden.
80
83
  - \`contract\` is the worker prompt — use it instead of assembling one by hand. It carries the acceptance criteria VERBATIM, the owned files, the dependency neighbourhood, the pre-located doc sections, EVERY matched capability skill with its RULES inline, the CodeGraph directive, and the rules the worker must not break.
81
84
  - \`contract\` warns when a REQUIRED capability skill is not installed yet — install it before dispatching.
82
85
  - \`contract\` returns a \`dispatchKey\`. Keep a ledger of \`dispatchKey → agent session\` so a follow-up CONTINUES that agent instead of starting a fresh one.
83
- - MAX 3 parallel workers at a time. A larger wave runs in batches of three.`,
86
+ - MAX 3 parallel workers at a time. A larger wave runs in batches of three.
87
+ - \`list\` and \`next\` narrow to ONE task with \`taskId\`; \`limit\`/\`offset\` bound the rest. \`meta\` says how many tasks exist.`,
84
88
 
85
89
  docs: `# archgen_docs
86
90
 
@@ -100,6 +104,11 @@ you need are usually a few hundred. \`digest\` gives you the section index, \`se
100
104
  | \`validate\` | reference-integrity gate | slugDir? |
101
105
 
102
106
  **Gotchas**
107
+ - A section may be named by its exact slug, by its title, or by any unambiguous PREFIX of the slug (\`plan-depth\` finds \`plan-depth-by-scope-class\`).
108
+ - A skill that is not installed reports the install call to make.
109
+ - **Batch calls with \`allSettled\`**, not \`all\`: one bad argument must not discard the results of its siblings.
110
+
111
+ ## Gotchas
103
112
  - \`map\` is section-level, \`refs\` is line-level. Use \`map\` to navigate, \`refs\` when you need exact lines.
104
113
  - \`section\` takes a BARE SECTION SLUG straight from \`digest\` — e.g. \`section:'non-functional-requirements'\`. It resolves the section across the documents. A slug that matches more than one document comes back as an \`ambiguous\` list, not a guess. \`path\` and \`path#anchor\` also work.
105
114
  - Anchors are validated against the target document's real heading slugs — a renamed section is a broken anchor, not a silent pass.
@@ -199,7 +208,8 @@ Matches a task against the capability-skill registry, installs EXACTLY the match
199
208
  - Exactly ONE skill is installed — never a bundle. A task that mentions "animation" gets the motion skill, not the whole UI family.
200
209
  - The installer writes \`<project>/.agents/skills/<slug>/SKILL.md\` — that is the file you point a worker at.
201
210
  - \`match\` reports \`required\` vs \`on-demand\`: a \`required\` match MUST be installed before dispatch.
202
- - \`load\` returns the skill's NORMATIVE RULES (the MUST / SHOULD / NEVER lines) and its section index. The rules are what a worker must follow — pull them from here rather than re-reading the file.
211
+ - \`load\` returns the NORMATIVE RULES (the MUST / SHOULD / NEVER lines) and the section index — NOT the whole file. The rules are what a worker must follow; pull them from here rather than re-reading the file. One section at a time: \`section:'<slug>'\`.
212
+ - \`list\` narrows to ONE registry row with \`skillId\`; \`digest\` narrows to ONE section with \`section\`.
203
213
  - A skill that is installed but never named in a worker contract is a skill that is never applied. \`archgen_tasks{action:'contract'}\` names EVERY matched required skill.`,
204
214
 
205
215
  refs: `# archgen_refs
@@ -221,7 +231,8 @@ Loads archgen's OWN protocol documents — the judgment layer: how to interview,
221
231
 
222
232
  **Gotchas**
223
233
  - Load the SECTION, not the topic: a topic is ~11 KB, a section is ~1-2 KB.
224
- - Section slugs are GitHub-exact (lowercase, punctuation dropped, whitespace to hyphens). Call \`index\` for the exact slug rather than guessing it.
234
+ - \`read\` with NO section returns the section INDEX — the map. \`section:'*'\` returns the whole topic, deliberately.
235
+ - A section may be named by its exact slug, by its title, or by any unambiguous PREFIX of the slug. The skill's own \`§\` names are prefixes — \`§artifact-templates/plan-depth\` resolves to \`plan-depth-by-scope-class\` — so pass the \`§\` name straight through. Call \`index\` only when a prefix is ambiguous.
225
236
  - \`code-standards.md\` is served too when the skill is installed — but WORKERS read it as a plain file, because they may have no MCP server at all.`,
226
237
 
227
238
  all: `# archgen — the complete tool surface
@@ -245,6 +256,12 @@ structured result. Every result is \`{ ok, data, error, meta }\` — \`ok:false\
245
256
  \`INVALID_INPUT\` · \`NOT_FOUND\` · \`CYCLE\` · \`OWNERSHIP_CONFLICT\` · \`CONFLICT\` · \`PERMISSION_DENIED\` ·
246
257
  \`UNAVAILABLE\` · \`TIMEOUT\` · \`INTERNAL\`
247
258
 
259
+ **Fetching, not dumping**
260
+ - Every tool takes \`limit\` and \`offset\` (default 50 items). Raise \`limit\` when you need more — the bound is yours, not the server's.
261
+ - Every collection narrows to ONE item: \`taskId\` (tasks, plan coverage), \`skillId\` (skills), \`file\` + \`section\` (docs), \`harness\` (setup).
262
+ - Every answer's \`meta\` carries \`available\` (what else exists), \`more\` (the call that fetches one item) and \`next\` (the exact call for the next slice).
263
+ - A bounded answer never hides data: the count is always in \`meta.total\`.
264
+
248
265
  **Call \`archgen_help { topic: "<domain>" }\`** for the full briefing on any one of them.`,
249
266
  };
250
267
 
@@ -16,6 +16,7 @@ import { verifyPlan, planCoverage } from '../core/verifier.mjs';
16
16
  import { planGraph } from '../core/plan-grapher.mjs';
17
17
  import { analyzeImpact } from '../core/impact-analyzer.mjs';
18
18
  import { invalidInput } from '../core/errors.mjs';
19
+ import { moreMeta, project, withMeta } from '../envelope.mjs';
19
20
 
20
21
  // The action list lives in archgen_help (one call away). Repeating it here
21
22
  // would tax every request's context for no extra information.
@@ -30,12 +31,14 @@ const schema = z.object({
30
31
  .describe('Absolute path to tasks.yaml. Required for validate, verify, impact and coverage.'),
31
32
  planDir: z.string().optional().describe('Absolute path to the plans directory (verify, coverage, brief).'),
32
33
  target: z.string().optional().describe('Task id or artifact path (impact).'),
34
+ taskId: z.string().optional().describe('ONE task id (coverage).'),
33
35
  node: z.string().optional().describe('Restrict the graph to one task neighborhood (graph).'),
34
36
  format: z.enum(['json', 'mermaid']).optional().describe('Graph output format. Defaults to json.'),
35
37
  status: z.boolean().optional().describe('Include status styling (graph, mermaid only).'),
36
38
  module: z.string().optional().describe('Restrict the graph to one module (graph).'),
37
39
  scope: z.enum(['SMALL', 'MEDIUM', 'LARGE']).optional().describe('Scope class (scaffold).'),
38
40
  slug: z.string().optional().describe('Project slug (scaffold, brief).'),
41
+ file: z.string().optional().describe('ONE artifact file (scaffold) — returns just that skeleton.'),
39
42
  kind: z.enum(BRIEF_KINDS).optional().describe('Which dispatch brief (brief).'),
40
43
  featureRequest: z
41
44
  .string()
@@ -43,6 +46,8 @@ const schema = z.object({
43
46
  .describe('The feature being requested, in one line (brief). Defaults to the slug.'),
44
47
  topic: z.string().optional().describe('Investigation topic (brief).'),
45
48
  round: z.number().int().positive().optional().describe('Investigation round number (brief).'),
49
+ limit: z.number().int().positive().optional().describe('Max items to return. Default 50.'),
50
+ offset: z.number().int().nonnegative().optional().describe('Skip this many items.'),
46
51
  projectRoot: projectRootArg,
47
52
  });
48
53
 
@@ -74,16 +79,53 @@ export function registerPlanTools(server) {
74
79
  case 'coverage': {
75
80
  const file = requireTasksYaml(args, ctx);
76
81
  if (!args.planDir) throw invalidInput('coverage requires planDir', { action });
77
- return planCoverage(file, ctx.contain(args.planDir));
82
+ const covered = planCoverage(file, ctx.contain(args.planDir));
83
+ // ONE task, when the caller named one.
84
+ if (args.taskId) {
85
+ const isCovered = (covered.covered ?? []).includes(args.taskId);
86
+ const isUncovered = (covered.uncovered ?? []).includes(args.taskId);
87
+ if (!isCovered && !isUncovered) {
88
+ throw invalidInput(`no task '${args.taskId}' in ${file}`, {
89
+ action,
90
+ taskId: args.taskId,
91
+ available: [...(covered.covered ?? []), ...(covered.uncovered ?? [])],
92
+ });
93
+ }
94
+ return { taskId: args.taskId, covered: isCovered, findings: covered.findings ?? [] };
95
+ }
96
+ const p = project([...(covered.covered ?? []), ...(covered.uncovered ?? [])], {
97
+ limit: args.limit,
98
+ offset: args.offset,
99
+ next: `archgen_plan{action:'coverage',tasksYaml:'${file}',planDir:'${args.planDir}',limit:${args.limit ?? 50},offset:${(args.offset ?? 0) + (args.limit ?? 50)}}`,
100
+ });
101
+ return withMeta(
102
+ { ...covered, covered: p.items.slice(0, (covered.covered ?? []).length) },
103
+ moreMeta({
104
+ total: p.total,
105
+ returned: p.returned,
106
+ next: p.next,
107
+ available: { tasks: p.total, uncovered: (covered.uncovered ?? []).length },
108
+ more: `archgen_plan{action:'coverage',tasksYaml:'${file}',planDir:'${args.planDir}',taskId:'<one task>'}`,
109
+ }),
110
+ );
78
111
  }
79
112
  case 'impact': {
80
113
  const file = requireTasksYaml(args, ctx);
81
114
  if (!args.target) throw invalidInput('impact requires target (a task id or an artifact path)', { action });
82
- return analyzeImpact(file, args.target);
115
+ const blast = analyzeImpact(file, args.target);
116
+ const p = project([...(blast.direct ?? []), ...(blast.transitive ?? [])], {
117
+ limit: args.limit,
118
+ offset: args.offset,
119
+ next: `archgen_plan{action:'impact',tasksYaml:'${file}',target:'${args.target}',limit:${args.limit ?? 50},offset:${(args.offset ?? 0) + (args.limit ?? 50)}}`,
120
+ });
121
+ return withMeta(
122
+ blast,
123
+ moreMeta({ total: p.total, returned: p.returned, next: p.next, available: { impacted: p.total } }),
124
+ );
83
125
  }
84
126
  case 'scaffold': {
85
127
  if (!args.scope) throw invalidInput('scaffold requires scope (SMALL | MEDIUM | LARGE)', { action });
86
- return buildScaffold(loadAsset('depth-ladder.json'), args.scope, {
128
+ const built = buildScaffold(loadAsset('depth-ladder.json'), args.scope, {
87
129
  slug: args.slug ?? null,
88
130
  templatesDir: TEMPLATES_DIR,
89
131
  // The generator token carries the RUNNING server's version. The
@@ -92,6 +134,29 @@ export function registerPlanTools(server) {
92
134
  // and no release ever has to update a version written by hand.
93
135
  generator: `archgen v${serverVersion()}`,
94
136
  });
137
+ // ONE file's skeleton, when the caller named one. The whole set is
138
+ // the CONTRACT — truncating it would produce a broken plan — so the
139
+ // default stays whole and the map to the rest lives in `meta`.
140
+ if (args.file) {
141
+ const key = Object.keys(built.skeletons ?? {}).find((k) => k === args.file || k.endsWith(`/${args.file}`));
142
+ if (!key) {
143
+ throw invalidInput(`no skeleton '${args.file}' for scope ${args.scope}`, {
144
+ action,
145
+ file: args.file,
146
+ available: Object.keys(built.skeletons ?? {}),
147
+ });
148
+ }
149
+ return { ...built, skeletons: { [key]: built.skeletons[key] }, one: key };
150
+ }
151
+ return withMeta(
152
+ built,
153
+ moreMeta({
154
+ total: (built.writeThese ?? []).length,
155
+ returned: (built.writeThese ?? []).length,
156
+ available: { files: (built.writeThese ?? []).length },
157
+ more: `archgen_plan{action:'scaffold',scope:'${args.scope}',slug:'${args.slug ?? '<slug>'}',file:'<one file>'}`,
158
+ }),
159
+ );
95
160
  }
96
161
  case 'brief': {
97
162
  if (!args.kind) throw invalidInput('brief requires kind', { action });
@@ -13,18 +13,24 @@ import { join } from 'node:path';
13
13
  import { registerTool, projectRootArg } from './_register.mjs';
14
14
  import { ASSETS_DIR } from '../context.mjs';
15
15
  import { indexTopic, readTopic, searchTopic, listTopics, REF_TOPICS } from '../core/refs.mjs';
16
+ import { moreMeta, project, withMeta } from '../envelope.mjs';
16
17
 
17
18
  const ACTION_DESC =
18
19
  'list = every reference topic with its section count; ' +
19
20
  'index = the section list of one topic; ' +
20
- 'read = ONE section (or the whole topic); ' +
21
+ 'read = ONE section (no section = the index; section:"*" = the whole topic); ' +
21
22
  'search = the sections mentioning a token.';
22
23
 
23
24
  const schema = z.object({
24
25
  action: z.enum(['list', 'index', 'read', 'search']).describe(ACTION_DESC),
25
26
  topic: z.enum(REF_TOPICS).optional().describe('Which reference (index, read, search).'),
26
- section: z.string().optional().describe('Section slug or heading (read). Omit for the whole topic.'),
27
+ section: z
28
+ .string()
29
+ .optional()
30
+ .describe('One section (read, index). "*" = the whole topic. Omit for the section index.'),
27
31
  token: z.string().optional().describe('The token to look for (search).'),
32
+ limit: z.number().int().positive().optional().describe('Max items to return. Default 50.'),
33
+ offset: z.number().int().nonnegative().optional().describe('Skip this many items.'),
28
34
  projectRoot: projectRootArg,
29
35
  });
30
36
 
@@ -43,20 +49,85 @@ export function registerRefTools(server) {
43
49
  idempotent: true,
44
50
  handler: (ctx, args) => {
45
51
  const roots = { assetsDir: join(ASSETS_DIR, 'references'), skillDir: ctx.skill?.path ?? null };
52
+ // The caller's bound, echoed into the exact next call so the agent
53
+ // never has to build the pagination itself.
54
+ const bound = args.limit ?? 50;
55
+ const from = args.offset ?? 0;
46
56
 
47
- if (args.action === 'list') return { topics: listTopics(roots) };
57
+ if (args.action === 'list') {
58
+ const p = project(listTopics(roots).topics, {
59
+ limit: args.limit,
60
+ offset: args.offset,
61
+ next: `archgen_refs{action:'list',limit:${bound},offset:${from + bound}}`,
62
+ });
63
+ return withMeta(
64
+ { topics: p.items },
65
+ moreMeta({ total: p.total, returned: p.returned, next: p.next, available: { topics: p.total } }),
66
+ );
67
+ }
48
68
 
49
69
  if (!args.topic) {
50
70
  throw invalidInput(`topic is required for '${args.action}'`, { action: args.action, known: REF_TOPICS });
51
71
  }
52
72
 
53
- if (args.action === 'index') return indexTopic(args.topic, roots);
73
+ if (args.action === 'index') {
74
+ const indexed = indexTopic(args.topic, { ...roots, section: args.section ?? null });
75
+ const p = project(indexed.sections, {
76
+ limit: args.limit,
77
+ offset: args.offset,
78
+ next: `archgen_refs{action:'index',topic:'${args.topic}',limit:${bound},offset:${from + bound}}`,
79
+ });
80
+ return withMeta(
81
+ { ...indexed, sections: p.items },
82
+ moreMeta({
83
+ total: p.total,
84
+ returned: p.returned,
85
+ next: p.next,
86
+ available: { sections: p.total },
87
+ more: `archgen_refs{action:'read',topic:'${args.topic}',section:'<the section you need>'}`,
88
+ }),
89
+ );
90
+ }
91
+
54
92
  if (args.action === 'search') {
55
93
  if (!args.token) throw invalidInput('search requires token', { action: args.action });
56
- return searchTopic(args.topic, args.token, roots);
94
+ const found = searchTopic(args.topic, args.token, roots);
95
+ const p = project(found.sections, {
96
+ limit: args.limit,
97
+ offset: args.offset,
98
+ next: `archgen_refs{action:'search',topic:'${args.topic}',token:'${args.token}',limit:${bound},offset:${from + bound}}`,
99
+ });
100
+ return withMeta(
101
+ { ...found, sections: p.items },
102
+ moreMeta({
103
+ total: p.total,
104
+ returned: p.returned,
105
+ next: p.next,
106
+ available: { sections: p.total, matches: found.matchCount },
107
+ more: `archgen_refs{action:'read',topic:'${args.topic}',section:'<the section you need>'}`,
108
+ }),
109
+ );
57
110
  }
58
- // read
59
- return readTopic(args.topic, { ...roots, section: args.section ?? null });
111
+
112
+ // read — ONE section by default; the index when no section is named;
113
+ // the whole topic only when the caller says '*'.
114
+ const read = readTopic(args.topic, { ...roots, section: args.section ?? null });
115
+ if (read.whole || read.text !== undefined) return read;
116
+ const p = project(read.sections, {
117
+ limit: args.limit,
118
+ offset: args.offset,
119
+ next: `archgen_refs{action:'read',topic:'${args.topic}',section:'<the section you need>',limit:${bound},offset:${from + bound}}`,
120
+ });
121
+ return withMeta(
122
+ { ...read, sections: p.items },
123
+ moreMeta({
124
+ total: p.total,
125
+ returned: p.returned,
126
+ next: p.next,
127
+ available: { sections: p.total },
128
+ more: `archgen_refs{action:'read',topic:'${args.topic}',section:'<the section you need>'}`,
129
+ }),
130
+ );
60
131
  },
61
132
  });
62
133
  }
@@ -6,12 +6,15 @@
6
6
  import { z } from 'zod/v4';
7
7
  import { registerTool, projectRootArg } from './_register.mjs';
8
8
  import { updateRegistry, showRegistry, DISPLAY_STATUSES } from '../core/registry-updater.mjs';
9
+ import { moreMeta, project, withMeta } from '../envelope.mjs';
9
10
 
10
11
  const schema = z.object({
11
12
  action: z.enum(['update', 'show']).describe('update = rewrite the registry from .archgen/; show = read it without writing.'),
12
13
  projectRoot: projectRootArg,
13
14
  slug: z.string().optional().describe('Upsert one feature row (update). Requires status.'),
14
15
  status: z.enum(DISPLAY_STATUSES).optional().describe('Registry status for the upserted row (update).'),
16
+ limit: z.number().int().positive().optional().describe('Max rows to return (show). Default 50; null for all.'),
17
+ offset: z.number().int().nonnegative().optional().describe('Skip this many rows (pagination).'),
15
18
  });
16
19
 
17
20
  /**
@@ -28,7 +31,18 @@ export function registerRegistryTools(server) {
28
31
  destructive: false,
29
32
  idempotent: true,
30
33
  handler: (ctx, args) => {
31
- if (args.action === 'show') return showRegistry(ctx.projectRoot);
34
+ if (args.action === 'show') {
35
+ const shown = showRegistry(ctx.projectRoot);
36
+ const p = project(shown.rows ?? [], {
37
+ limit: args.limit,
38
+ offset: args.offset,
39
+ next: `archgen_registry{action:'show',limit:${args.limit ?? 50},offset:${(args.offset ?? 0) + (args.limit ?? 50)}}`,
40
+ });
41
+ return withMeta(
42
+ { ...shown, rows: p.items },
43
+ moreMeta({ total: p.total, returned: p.returned, next: p.next, available: { rows: p.total } }),
44
+ );
45
+ }
32
46
  return updateRegistry(ctx.projectRoot, { slug: args.slug ?? null, status: args.status ?? null });
33
47
  },
34
48
  });
@@ -7,6 +7,8 @@ import { manageCodegraph, assertLocation, findCodegraph } from '../core/codegrap
7
7
  import { detectHarnesses } from '../core/harness-wiring.mjs';
8
8
  import { platformCapabilities, detectHarness } from '../core/platform-caps.mjs';
9
9
  import { checkCapability } from '../core/compat.mjs';
10
+ import { invalidInput } from '../core/errors.mjs';
11
+ import { moreMeta, project, withMeta } from '../envelope.mjs';
10
12
 
11
13
  const ACTION_DESC =
12
14
  'doctor | bootstrap | install_mcp | uninstall_mcp | harnesses | harness_info — see archgen_help.';
@@ -16,7 +18,9 @@ const schema = z.object({
16
18
  location: z.enum(['project', 'global', 'both']).optional().describe('Which scope to wire. Defaults to both.'),
17
19
  approve: z.boolean().optional().describe('Required to write any MCP config. Without it you get the plan and nothing is written.'),
18
20
  telemetry: z.enum(['keep', 'off', 'on']).optional().describe('CodeGraph telemetry preference (bootstrap).'),
19
- harness: z.string().optional().describe('Harness id (harness_info). Defaults to the detected one.'),
21
+ harness: z.string().optional().describe('Harness id (harness_info, harnesses). One harness (harnesses).'),
22
+ limit: z.number().int().positive().optional().describe('Max items to return. Default 50.'),
23
+ offset: z.number().int().nonnegative().optional().describe('Skip this many items.'),
20
24
  projectRoot: projectRootArg,
21
25
  });
22
26
 
@@ -58,23 +62,56 @@ export function registerSetupTools(server) {
58
62
  }
59
63
 
60
64
  if (args.action === 'harnesses') {
61
- const detected = detectHarnesses(registry, ctx.projectRoot, ctx.home);
62
- return {
63
- location,
64
- detected: detected.map((d) => ({
65
- id: d.id,
66
- label: d.label,
67
- atHome: d.atHome,
68
- atProject: d.atProject,
69
- configs: Object.entries(d.spec.configs ?? {}).map(([loc, cfg]) => ({
70
- location: loc,
71
- path: cfg.path,
72
- key: cfg.key ?? `mcp_servers.archgen`,
73
- format: cfg.format ?? 'json',
74
- })),
65
+ const detected = detectHarnesses(registry, ctx.projectRoot, ctx.home, ctx.env);
66
+ const rows = detected.map((d) => ({
67
+ id: d.id,
68
+ label: d.label,
69
+ atHome: d.atHome,
70
+ atProject: d.atProject,
71
+ configs: Object.entries(d.spec.configs ?? {}).map(([loc, cfg]) => ({
72
+ location: loc,
73
+ path: cfg.path,
74
+ key: cfg.key ?? `mcp_servers.archgen`,
75
+ format: cfg.format ?? 'json',
75
76
  })),
76
- binary: resolveMcpBinary({ projectRoot: ctx.projectRoot, home: ctx.home, env: ctx.env }),
77
- };
77
+ }));
78
+ // ONE harness, when the caller named one.
79
+ if (args.harness) {
80
+ const one = rows.find((r) => r.id === args.harness);
81
+ if (!one) {
82
+ throw invalidInput(`unknown harness '${args.harness}'`, {
83
+ action: args.action,
84
+ harness: args.harness,
85
+ available: rows.map((r) => r.id),
86
+ });
87
+ }
88
+ return {
89
+ location,
90
+ detected: [one],
91
+ binary: resolveMcpBinary({ projectRoot: ctx.projectRoot, home: ctx.home, env: ctx.env }),
92
+ };
93
+ }
94
+ const p = project(rows, {
95
+ limit: args.limit,
96
+ offset: args.offset,
97
+ next: `archgen_setup{action:'harnesses',limit:${args.limit ?? 50},offset:${(args.offset ?? 0) + (args.limit ?? 50)}}`,
98
+ });
99
+ return withMeta(
100
+ {
101
+ location,
102
+ // ONE copy of the rows: the same array under two keys was the
103
+ // payload saying the same thing twice.
104
+ detected: p.items,
105
+ binary: resolveMcpBinary({ projectRoot: ctx.projectRoot, home: ctx.home, env: ctx.env }),
106
+ },
107
+ moreMeta({
108
+ total: p.total,
109
+ returned: p.returned,
110
+ next: p.next,
111
+ available: { harnesses: p.total },
112
+ more: `archgen_setup{action:'harnesses',harness:'<one harness>'}`,
113
+ }),
114
+ );
78
115
  }
79
116
 
80
117
  if (args.action === 'doctor') {
@@ -130,7 +167,7 @@ export function registerSetupTools(server) {
130
167
  // project's wiring in place, and vice versa.
131
168
  return {
132
169
  written: true,
133
- ...uninstallArchgenMcp(ctx.projectRoot, ctx.home, location),
170
+ ...uninstallArchgenMcp(ctx.projectRoot, ctx.home, location, ctx.env),
134
171
  };
135
172
  }
136
173