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.
- package/README.md +14 -0
- package/assets/briefs/survey.md +1 -1
- package/assets/briefs/verifier.md +2 -1
- package/assets/harness-mcp.json +10 -1
- package/assets/platform-capabilities.json +11 -0
- package/assets/references/artifact-templates.md +22 -0
- package/assets/references/mcp-and-skills.md +3 -1
- package/assets/references/platforms.md +9 -1
- package/assets/templates/architecture.yaml +9 -16
- package/assets/templates/tasks.yaml +2 -2
- package/package.json +1 -1
- package/src/cli.mjs +2 -2
- package/src/core/codegraph-manager.mjs +2 -2
- package/src/core/doc-navigator.mjs +29 -6
- package/src/core/docs.mjs +24 -2
- package/src/core/fs-safety.mjs +64 -45
- package/src/core/guidance.mjs +142 -0
- package/src/core/harness-wiring.mjs +93 -9
- package/src/core/mcp-server-manager.mjs +4 -2
- package/src/core/plan-grapher.mjs +2 -1
- package/src/core/refs.mjs +91 -21
- package/src/core/scaffold.mjs +6 -0
- package/src/core/worker-contract.mjs +3 -2
- package/src/envelope.mjs +98 -1
- package/src/tools/codegraph.mjs +20 -1
- package/src/tools/docs.mjs +65 -12
- package/src/tools/help.mjs +22 -5
- 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 +55 -18
- package/src/tools/skills.mjs +148 -40
- package/src/tools/tasks.mjs +68 -3
package/src/tools/codegraph.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/tools/docs.mjs
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
61
|
+
}
|
|
62
|
+
case 'refs': {
|
|
46
63
|
if (!args.token) throw invalidInput('refs requires a token', { action: args.action });
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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;
|
package/src/tools/help.mjs
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|