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.
- package/README.md +14 -0
- package/assets/briefs/survey.md +1 -1
- package/assets/briefs/verifier.md +2 -1
- package/assets/references/artifact-templates.md +22 -0
- package/assets/references/mcp-and-skills.md +3 -1
- package/assets/templates/architecture.yaml +9 -16
- package/assets/templates/tasks.yaml +2 -2
- package/package.json +1 -1
- package/src/core/doc-navigator.mjs +24 -5
- package/src/core/refs.mjs +89 -16
- package/src/envelope.mjs +98 -1
- package/src/tools/codegraph.mjs +20 -1
- package/src/tools/docs.mjs +55 -10
- package/src/tools/help.mjs +13 -3
- package/src/tools/plan.mjs +68 -3
- package/src/tools/refs.mjs +78 -7
- package/src/tools/registry.mjs +15 -1
- package/src/tools/setup.mjs +53 -16
- package/src/tools/skills.mjs +135 -37
- package/src/tools/tasks.mjs +68 -3
package/src/tools/help.mjs
CHANGED
|
@@ -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
|
|
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
|
|
package/src/tools/plan.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|
package/src/tools/refs.mjs
CHANGED
|
@@ -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 (
|
|
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
|
|
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')
|
|
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')
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
}
|
package/src/tools/registry.mjs
CHANGED
|
@@ -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')
|
|
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
|
});
|
package/src/tools/setup.mjs
CHANGED
|
@@ -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).
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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') {
|
package/src/tools/skills.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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({
|