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.
@@ -47,7 +47,8 @@ Validates and analyzes a plan (tasks.yaml + plans/).
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
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
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.`,
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.`,
51
52
 
52
53
  tasks: `# archgen_tasks
53
54
 
@@ -82,7 +83,8 @@ Dispatching the next wave while any of those lag is forbidden.
82
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.
83
84
  - \`contract\` warns when a REQUIRED capability skill is not installed yet — install it before dispatching.
84
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.
85
- - 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.`,
86
88
 
87
89
  docs: `# archgen_docs
88
90
 
@@ -206,7 +208,8 @@ Matches a task against the capability-skill registry, installs EXACTLY the match
206
208
  - Exactly ONE skill is installed — never a bundle. A task that mentions "animation" gets the motion skill, not the whole UI family.
207
209
  - The installer writes \`<project>/.agents/skills/<slug>/SKILL.md\` — that is the file you point a worker at.
208
210
  - \`match\` reports \`required\` vs \`on-demand\`: a \`required\` match MUST be installed before dispatch.
209
- - \`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\`.
210
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.`,
211
214
 
212
215
  refs: `# archgen_refs
@@ -228,6 +231,7 @@ Loads archgen's OWN protocol documents — the judgment layer: how to interview,
228
231
 
229
232
  **Gotchas**
230
233
  - Load the SECTION, not the topic: a topic is ~11 KB, a section is ~1-2 KB.
234
+ - \`read\` with NO section returns the section INDEX — the map. \`section:'*'\` returns the whole topic, deliberately.
231
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.
232
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.`,
233
237
 
@@ -252,6 +256,12 @@ structured result. Every result is \`{ ok, data, error, meta }\` — \`ok:false\
252
256
  \`INVALID_INPUT\` · \`NOT_FOUND\` · \`CYCLE\` · \`OWNERSHIP_CONFLICT\` · \`CONFLICT\` · \`PERMISSION_DENIED\` ·
253
257
  \`UNAVAILABLE\` · \`TIMEOUT\` · \`INTERNAL\`
254
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
+
255
265
  **Call \`archgen_help { topic: "<domain>" }\`** for the full briefing on any one of them.`,
256
266
  };
257
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
 
@@ -59,22 +63,55 @@ export function registerSetupTools(server) {
59
63
 
60
64
  if (args.action === 'harnesses') {
61
65
  const detected = detectHarnesses(registry, ctx.projectRoot, ctx.home, ctx.env);
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
- })),
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') {
@@ -11,6 +11,7 @@ import { registerTool, projectRootArg } from './_register.mjs';
11
11
  import { loadAsset } from '../context.mjs';
12
12
  import { invalidInput } from '../core/errors.mjs';
13
13
  import { hint, teach } from '../core/guidance.mjs';
14
+ import { moreMeta, project, withMeta } from '../envelope.mjs';
14
15
  import {
15
16
  matchSkills,
16
17
  installSkill,
@@ -36,11 +37,16 @@ const schema = z.object({
36
37
  title: z.string().optional().describe('Task title (match).'),
37
38
  summary: z.string().optional().describe('Task summary (match).'),
38
39
  fileOwnership: z.array(z.string()).optional().describe('Task file_ownership globs (match).'),
39
- skillId: z.string().optional().describe('Registry skill id or slug (install, load, digest).'),
40
- section: z.string().optional().describe('One section of the skill (load). Omit for the whole skill.'),
40
+ skillId: z.string().optional().describe('Registry skill id or slug (install, load, digest, list).'),
41
+ section: z
42
+ .string()
43
+ .optional()
44
+ .describe('One section of the skill (load, digest). Omit for the section index.'),
41
45
  harness: z.string().optional().describe('Harness id to install for (install). Defaults to universal.'),
42
46
  global: z.boolean().optional().describe('Install for the user instead of the project (install).'),
43
47
  approve: z.boolean().optional().describe('Required to install. Without it you get the plan and nothing is written.'),
48
+ limit: z.number().int().positive().optional().describe('Max items to return. Default 50.'),
49
+ offset: z.number().int().nonnegative().optional().describe('Skip this many items.'),
44
50
  projectRoot: projectRootArg,
45
51
  });
46
52
 
@@ -60,21 +66,39 @@ export function registerSkillTools(server) {
60
66
  idempotent: true,
61
67
  handler: (ctx, args) => {
62
68
  const registry = loadAsset('skill-registry.json');
69
+ const bound = args.limit ?? 50;
70
+ const from = args.offset ?? 0;
63
71
 
64
72
  if (args.action === 'list') {
65
- return {
66
- version: registry.version,
67
- installer: registry.installer,
68
- skills: registry.skills.map((s) => ({
69
- id: s.id,
70
- slug: s.slug,
71
- family: s.family,
72
- repo: s.repo,
73
- purpose: s.purpose,
74
- policy: s.policy,
75
- matches: s.matches,
76
- })),
77
- };
73
+ // ONE row, or the whole registry — but never three copies of a row.
74
+ const rows = registry.skills.map((s) => ({
75
+ id: s.id,
76
+ slug: s.slug,
77
+ family: s.family,
78
+ repo: s.repo,
79
+ purpose: s.purpose,
80
+ policy: s.policy,
81
+ matches: s.matches,
82
+ }));
83
+ if (args.skillId) {
84
+ const one = rows.find((s) => s.id === args.skillId || s.slug === args.skillId);
85
+ if (!one) {
86
+ throw invalidInput(
87
+ teach(`unknown skill '${args.skillId}'`, 'the registry lists every id and slug', "archgen_skills{action:'list'}"),
88
+ hint({ action: "archgen_skills{action:'list'}", reason: 'the registry is the vocabulary.' }),
89
+ );
90
+ }
91
+ return { version: registry.version, installer: registry.installer, skill: one, skills: [one] };
92
+ }
93
+ const p = project(rows, {
94
+ limit: args.limit,
95
+ offset: args.offset,
96
+ next: `archgen_skills{action:'list',limit:${bound},offset:${from + bound}}`,
97
+ });
98
+ return withMeta(
99
+ { version: registry.version, installer: registry.installer, skills: p.items },
100
+ moreMeta({ total: p.total, returned: p.returned, next: p.next, available: { skills: p.total } }),
101
+ );
78
102
  }
79
103
 
80
104
  if (args.action === 'match') {
@@ -83,21 +107,47 @@ export function registerSkillTools(server) {
83
107
  summary: args.summary,
84
108
  fileOwnership: args.fileOwnership,
85
109
  });
86
- return {
87
- matched,
88
- required: matched.filter((m) => m.policy === 'required'),
89
- installed: matched.map((m) => {
90
- const found = findInstalledSkill(ctx.projectRoot, m.slug, { home: ctx.home });
91
- return { ...m, installed: found.installed, path: found.path, scope: found.scope };
110
+ // ONE copy of each row: the install state rides ON the row, instead of
111
+ // the same row being delivered three times under three keys.
112
+ const rows = matched.map((m) => {
113
+ const found = findInstalledSkill(ctx.projectRoot, m.slug, { home: ctx.home });
114
+ return { ...m, installed: found.installed, path: found.path, scope: found.scope };
115
+ });
116
+ const p = project(rows, {
117
+ limit: args.limit,
118
+ offset: args.offset,
119
+ next: `archgen_skills{action:'match',title:'${args.title ?? ''}',limit:${bound},offset:${from + bound}}`,
120
+ });
121
+ return withMeta(
122
+ {
123
+ matched: p.items,
124
+ // The required ones, by id — the rows are already above.
125
+ required: p.items.filter((m) => m.policy === 'required').map((m) => m.slug),
126
+ installed: p.items.filter((m) => m.installed).map((m) => m.slug),
127
+ note: matched.length
128
+ ? 'Install every required skill before dispatching the worker.'
129
+ : 'No capability domain matched — no external skill is needed.',
130
+ },
131
+ moreMeta({
132
+ total: p.total,
133
+ returned: p.returned,
134
+ next: p.next,
135
+ available: { skills: p.total, required: p.items.filter((m) => m.policy === 'required').length },
92
136
  }),
93
- note: matched.length
94
- ? 'Install every required skill before dispatching the worker.'
95
- : 'No capability domain matched — no external skill is needed.',
96
- };
137
+ );
97
138
  }
98
139
 
99
140
  if (args.action === 'installed') {
100
- return { installed: listInstalled({ projectRoot: ctx.projectRoot, registry, home: ctx.home }) };
141
+ const all = listInstalled({ projectRoot: ctx.projectRoot, registry, home: ctx.home });
142
+ const p = project(all.installed ?? [], {
143
+ limit: args.limit,
144
+ offset: args.offset,
145
+ next: `archgen_skills{action:'installed',limit:${bound},offset:${from + bound}}`,
146
+ });
147
+ return withMeta(
148
+ { installed: p.items },
149
+ moreMeta({ total: p.total, returned: p.returned, next: p.next, available: { skills: p.total } }),
150
+ );
101
151
  }
102
152
 
103
153
  // install | load
@@ -134,13 +184,40 @@ export function registerSkillTools(server) {
134
184
  };
135
185
  }
136
186
  const content = readFileSync(found.path, 'utf8');
137
- return {
138
- skill: entry.slug,
139
- installed: true,
140
- path: found.path,
141
- scope: found.scope,
142
- sections: skillSections(content),
143
- };
187
+ let sections = skillSections(content);
188
+ // ONE section, when the caller named one.
189
+ if (args.section) {
190
+ const wanted = String(args.section).trim().toLowerCase();
191
+ const hit = sections.find(
192
+ (s) =>
193
+ s.slug === args.section ||
194
+ s.title.toLowerCase() === wanted ||
195
+ s.slug.replace(/-{2,}/g, '-') === wanted.replace(/-{2,}/g, '-'),
196
+ );
197
+ if (!hit) {
198
+ throw invalidInput(`no section '${args.section}' in skill '${entry.slug}'`, {
199
+ skillId: entry.slug,
200
+ section: args.section,
201
+ available: sections.map((s) => s.slug),
202
+ });
203
+ }
204
+ sections = [hit];
205
+ }
206
+ const p = project(sections, {
207
+ limit: args.limit,
208
+ offset: args.offset,
209
+ next: `archgen_skills{action:'digest',skillId:'${entry.slug}',limit:${bound},offset:${from + bound}}`,
210
+ });
211
+ return withMeta(
212
+ { skill: entry.slug, installed: true, path: found.path, scope: found.scope, sections: p.items },
213
+ moreMeta({
214
+ total: p.total,
215
+ returned: p.returned,
216
+ next: p.next,
217
+ available: { sections: p.total },
218
+ more: `archgen_skills{action:'load',skillId:'${entry.slug}',section:'<the section you need>'}`,
219
+ }),
220
+ );
144
221
  }
145
222
 
146
223
  if (args.action === 'load') {
@@ -152,9 +229,14 @@ export function registerSkillTools(server) {
152
229
  skill: entry.slug,
153
230
  purpose: entry.purpose,
154
231
  domain: entry.domain ?? entry.family ?? 'Capability',
155
- ...loaded,
156
- // The normative rules, and the section index — the agent decides
157
- // what to pull from here.
232
+ loaded: loaded.loaded,
233
+ path: loaded.path,
234
+ scope: loaded.scope,
235
+ // The RAW CONTENT IS NOT RETURNED. It used to be: the file arrived
236
+ // three times in one call — as `content`, as the extracted `rules`
237
+ // and as the section index. The rules are what the agent must obey
238
+ // and the index is what it may pull; the file itself is one call
239
+ // away (`section:'<slug>'`), and the agent is told where it is.
158
240
  rules: rules.rules,
159
241
  ruleCount: rules.count,
160
242
  sections: sections.map((s) => ({
@@ -169,6 +251,8 @@ export function registerSkillTools(server) {
169
251
  ? null
170
252
  : 'not installed — install it first: archgen_skills{action:\'install\',skillId:\'' + entry.slug + '\',approve:true}',
171
253
  };
254
+ // Where the tool looked matters only when it found nothing.
255
+ if (!loaded.loaded) result.searchedPaths = loaded.searchedPaths ?? [];
172
256
 
173
257
  // ONE section — the agent's lens, the same way archgen_docs serves
174
258
  // one section of an artifact.
@@ -187,7 +271,21 @@ export function registerSkillTools(server) {
187
271
  const body = loaded.content.split('\n').slice(hit.line - 1, hit.endLine).join('\n');
188
272
  return { ...result, section: hit.slug, sectionTitle: hit.title, sectionText: body };
189
273
  }
190
- return result;
274
+ const p = project(result.sections, {
275
+ limit: args.limit,
276
+ offset: args.offset,
277
+ next: `archgen_skills{action:'load',skillId:'${entry.slug}',limit:${bound},offset:${from + bound}}`,
278
+ });
279
+ return withMeta(
280
+ { ...result, sections: p.items },
281
+ moreMeta({
282
+ total: p.total,
283
+ returned: p.returned,
284
+ next: p.next,
285
+ available: { sections: p.total },
286
+ more: `archgen_skills{action:'load',skillId:'${entry.slug}',section:'<the section you need>'}`,
287
+ }),
288
+ );
191
289
  }
192
290
 
193
291
  return installSkill({