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/skills.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import { readFileSync } from 'node:fs';
|
|
|
10
10
|
import { registerTool, projectRootArg } from './_register.mjs';
|
|
11
11
|
import { loadAsset } from '../context.mjs';
|
|
12
12
|
import { invalidInput } from '../core/errors.mjs';
|
|
13
|
+
import { hint, teach } from '../core/guidance.mjs';
|
|
14
|
+
import { moreMeta, project, withMeta } from '../envelope.mjs';
|
|
13
15
|
import {
|
|
14
16
|
matchSkills,
|
|
15
17
|
installSkill,
|
|
@@ -35,11 +37,16 @@ const schema = z.object({
|
|
|
35
37
|
title: z.string().optional().describe('Task title (match).'),
|
|
36
38
|
summary: z.string().optional().describe('Task summary (match).'),
|
|
37
39
|
fileOwnership: z.array(z.string()).optional().describe('Task file_ownership globs (match).'),
|
|
38
|
-
skillId: z.string().optional().describe('Registry skill id or slug (install, load, digest).'),
|
|
39
|
-
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.'),
|
|
40
45
|
harness: z.string().optional().describe('Harness id to install for (install). Defaults to universal.'),
|
|
41
46
|
global: z.boolean().optional().describe('Install for the user instead of the project (install).'),
|
|
42
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.'),
|
|
43
50
|
projectRoot: projectRootArg,
|
|
44
51
|
});
|
|
45
52
|
|
|
@@ -59,21 +66,39 @@ export function registerSkillTools(server) {
|
|
|
59
66
|
idempotent: true,
|
|
60
67
|
handler: (ctx, args) => {
|
|
61
68
|
const registry = loadAsset('skill-registry.json');
|
|
69
|
+
const bound = args.limit ?? 50;
|
|
70
|
+
const from = args.offset ?? 0;
|
|
62
71
|
|
|
63
72
|
if (args.action === 'list') {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
);
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
if (args.action === 'match') {
|
|
@@ -82,21 +107,47 @@ export function registerSkillTools(server) {
|
|
|
82
107
|
summary: args.summary,
|
|
83
108
|
fileOwnership: args.fileOwnership,
|
|
84
109
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 },
|
|
91
136
|
}),
|
|
92
|
-
|
|
93
|
-
? 'Install every required skill before dispatching the worker.'
|
|
94
|
-
: 'No capability domain matched — no external skill is needed.',
|
|
95
|
-
};
|
|
137
|
+
);
|
|
96
138
|
}
|
|
97
139
|
|
|
98
140
|
if (args.action === 'installed') {
|
|
99
|
-
|
|
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
|
+
);
|
|
100
151
|
}
|
|
101
152
|
|
|
102
153
|
// install | load
|
|
@@ -108,7 +159,14 @@ export function registerSkillTools(server) {
|
|
|
108
159
|
? registry.routing
|
|
109
160
|
: null);
|
|
110
161
|
if (!entry) {
|
|
111
|
-
throw invalidInput(
|
|
162
|
+
throw invalidInput(
|
|
163
|
+
teach(
|
|
164
|
+
`unknown skill '${wanted}'`,
|
|
165
|
+
'the registry lists every id and slug',
|
|
166
|
+
"archgen_skills{action:'list'}",
|
|
167
|
+
),
|
|
168
|
+
hint({ action: "archgen_skills{action:'list'}", reason: 'the registry is the vocabulary.' }),
|
|
169
|
+
);
|
|
112
170
|
}
|
|
113
171
|
|
|
114
172
|
if (args.action === 'digest') {
|
|
@@ -122,17 +180,44 @@ export function registerSkillTools(server) {
|
|
|
122
180
|
installed: false,
|
|
123
181
|
sections: [],
|
|
124
182
|
searchedPaths: found.searchedPaths ?? [],
|
|
125
|
-
hint: 'not installed —
|
|
183
|
+
hint: 'not installed — install it first: archgen_skills{action:\'install\',skillId:\'' + entry.slug + '\',approve:true}',
|
|
126
184
|
};
|
|
127
185
|
}
|
|
128
186
|
const content = readFileSync(found.path, 'utf8');
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
);
|
|
136
221
|
}
|
|
137
222
|
|
|
138
223
|
if (args.action === 'load') {
|
|
@@ -144,9 +229,14 @@ export function registerSkillTools(server) {
|
|
|
144
229
|
skill: entry.slug,
|
|
145
230
|
purpose: entry.purpose,
|
|
146
231
|
domain: entry.domain ?? entry.family ?? 'Capability',
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
150
240
|
rules: rules.rules,
|
|
151
241
|
ruleCount: rules.count,
|
|
152
242
|
sections: sections.map((s) => ({
|
|
@@ -157,8 +247,12 @@ export function registerSkillTools(server) {
|
|
|
157
247
|
normative: s.normative,
|
|
158
248
|
})),
|
|
159
249
|
followInstruction: loaded.loaded ? followInstruction({ slug: entry.slug, path: loaded.path, domain: entry.domain }) : null,
|
|
160
|
-
hint: loaded.loaded
|
|
250
|
+
hint: loaded.loaded
|
|
251
|
+
? null
|
|
252
|
+
: 'not installed — install it first: archgen_skills{action:\'install\',skillId:\'' + entry.slug + '\',approve:true}',
|
|
161
253
|
};
|
|
254
|
+
// Where the tool looked matters only when it found nothing.
|
|
255
|
+
if (!loaded.loaded) result.searchedPaths = loaded.searchedPaths ?? [];
|
|
162
256
|
|
|
163
257
|
// ONE section — the agent's lens, the same way archgen_docs serves
|
|
164
258
|
// one section of an artifact.
|
|
@@ -177,7 +271,21 @@ export function registerSkillTools(server) {
|
|
|
177
271
|
const body = loaded.content.split('\n').slice(hit.line - 1, hit.endLine).join('\n');
|
|
178
272
|
return { ...result, section: hit.slug, sectionTitle: hit.title, sectionText: body };
|
|
179
273
|
}
|
|
180
|
-
|
|
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
|
+
);
|
|
181
289
|
}
|
|
182
290
|
|
|
183
291
|
return installSkill({
|
package/src/tools/tasks.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { loadAsset } from '../context.mjs';
|
|
|
11
11
|
import { setTaskStatus, STATUSES } from '../core/status-mutator.mjs';
|
|
12
12
|
import { writeResumeSnapshot } from '../core/resume-snapshot.mjs';
|
|
13
13
|
import { invalidInput } from '../core/errors.mjs';
|
|
14
|
+
import { moreMeta, project, withMeta } from '../envelope.mjs';
|
|
14
15
|
import { resolveSlugInput, ScopeError } from '../core/scope.mjs';
|
|
15
16
|
import { dirname, join } from 'node:path';
|
|
16
17
|
|
|
@@ -40,13 +41,15 @@ const ACTION_DESC = 'next | set_status | list | snapshot | contract — see arch
|
|
|
40
41
|
const schema = z.object({
|
|
41
42
|
action: z.enum(['next', 'set_status', 'list', 'snapshot', 'contract']).describe(ACTION_DESC),
|
|
42
43
|
tasksYaml: z.string().optional().describe('Absolute path to tasks.yaml. Required for next, set_status and list.'),
|
|
43
|
-
taskId: z.string().optional().describe('Task id (set_status).'),
|
|
44
|
+
taskId: z.string().optional().describe('Task id (set_status, contract). ONE task (list, next).'),
|
|
44
45
|
status: z.enum(STATUSES).optional().describe('New status (set_status).'),
|
|
45
46
|
force: z.boolean().optional().describe('Allow reopening a done task (set_status).'),
|
|
46
47
|
filterStatus: z.string().optional().describe('Filter by status (list).'),
|
|
47
48
|
slugDir: z.string().optional().describe('The .archgen/<slug>/ directory (contract) — locates the plan docs.'),
|
|
48
49
|
codeStandardsPath: z.string().optional().describe('Path to the code-standards reference (contract).'),
|
|
49
50
|
module: z.string().optional().describe('Filter by module path fragment (list).'),
|
|
51
|
+
limit: z.number().int().positive().optional().describe('Max items to return. Default 50.'),
|
|
52
|
+
offset: z.number().int().nonnegative().optional().describe('Skip this many items.'),
|
|
50
53
|
projectRoot: projectRootArg,
|
|
51
54
|
});
|
|
52
55
|
|
|
@@ -74,10 +77,37 @@ export function registerTaskTools(server) {
|
|
|
74
77
|
}
|
|
75
78
|
case 'list': {
|
|
76
79
|
if (!args.tasksYaml) throw invalidInput('tasksYaml is required for list', { action });
|
|
77
|
-
|
|
80
|
+
const listed = listTasks(ctx.contain(args.tasksYaml), {
|
|
78
81
|
status: args.filterStatus ?? null,
|
|
79
82
|
module: args.module ?? null,
|
|
80
83
|
});
|
|
84
|
+
// ONE task, when the caller named one.
|
|
85
|
+
if (args.taskId) {
|
|
86
|
+
const one = (listed.tasks ?? []).find((t) => t.id === args.taskId);
|
|
87
|
+
if (!one) {
|
|
88
|
+
throw invalidInput(`no task '${args.taskId}' in ${args.tasksYaml}`, {
|
|
89
|
+
action,
|
|
90
|
+
taskId: args.taskId,
|
|
91
|
+
available: (listed.tasks ?? []).map((t) => t.id),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return { task: one, tasks: [one] };
|
|
95
|
+
}
|
|
96
|
+
const p = project(listed.tasks ?? [], {
|
|
97
|
+
limit: args.limit,
|
|
98
|
+
offset: args.offset,
|
|
99
|
+
next: `archgen_tasks{action:'list',tasksYaml:'${args.tasksYaml}',limit:${args.limit ?? 50},offset:${(args.offset ?? 0) + (args.limit ?? 50)}}`,
|
|
100
|
+
});
|
|
101
|
+
return withMeta(
|
|
102
|
+
{ ...listed, tasks: p.items },
|
|
103
|
+
moreMeta({
|
|
104
|
+
total: p.total,
|
|
105
|
+
returned: p.returned,
|
|
106
|
+
next: p.next,
|
|
107
|
+
available: { tasks: p.total },
|
|
108
|
+
more: `archgen_tasks{action:'list',tasksYaml:'${args.tasksYaml}',taskId:'<one task>'}`,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
81
111
|
}
|
|
82
112
|
case 'snapshot': {
|
|
83
113
|
if (!args.tasksYaml) throw invalidInput('tasksYaml is required for snapshot', { action });
|
|
@@ -196,7 +226,42 @@ export function registerTaskTools(server) {
|
|
|
196
226
|
}
|
|
197
227
|
case 'next': {
|
|
198
228
|
if (!args.tasksYaml) throw invalidInput('tasksYaml is required for next', { action });
|
|
199
|
-
|
|
229
|
+
const resolved = resolveWaves(ctx.contain(args.tasksYaml));
|
|
230
|
+
// ONE wave, when the caller named a task: "which wave is this in?"
|
|
231
|
+
if (args.taskId) {
|
|
232
|
+
const index = resolved.waves.findIndex((w) => w.some((t) => t.id === args.taskId));
|
|
233
|
+
if (index < 0) {
|
|
234
|
+
throw invalidInput(`no task '${args.taskId}' in ${args.tasksYaml}`, {
|
|
235
|
+
action,
|
|
236
|
+
taskId: args.taskId,
|
|
237
|
+
available: resolved.waves.flat().map((t) => t.id),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
waveIndex: index,
|
|
242
|
+
wave: resolved.waves[index],
|
|
243
|
+
waves: [resolved.waves[index]],
|
|
244
|
+
blockedByFailure: resolved.blockedByFailure,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// The waves are the plan's shape; a long plan would dump every task
|
|
248
|
+
// in every wave, so the wave COUNT is reported and the waves
|
|
249
|
+
// themselves are bounded.
|
|
250
|
+
const p = project(resolved.waves, {
|
|
251
|
+
limit: args.limit,
|
|
252
|
+
offset: args.offset,
|
|
253
|
+
next: `archgen_tasks{action:'next',tasksYaml:'${args.tasksYaml}',limit:${args.limit ?? 50},offset:${(args.offset ?? 0) + (args.limit ?? 50)}}`,
|
|
254
|
+
});
|
|
255
|
+
return withMeta(
|
|
256
|
+
{ ...resolved, waves: p.items },
|
|
257
|
+
moreMeta({
|
|
258
|
+
total: p.total,
|
|
259
|
+
returned: p.returned,
|
|
260
|
+
next: p.next,
|
|
261
|
+
available: { waves: p.total, tasks: resolved.waves.flat().length },
|
|
262
|
+
more: `archgen_tasks{action:'next',tasksYaml:'${args.tasksYaml}',taskId:'<one task>'}`,
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
200
265
|
}
|
|
201
266
|
default:
|
|
202
267
|
throw invalidInput(`unknown action '${action}'`, { action });
|