@zerotal/arch 1.7.5 → 1.8.0
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/CHANGELOG.md +52 -0
- package/api-surface.md +2 -0
- package/docs/changelog.md +163 -0
- package/docs/commands.md +32 -0
- package/docs/deployment.md +97 -0
- package/docs/flow/index.md +1 -1
- package/docs/flow/layouts.md +64 -0
- package/docs/testing/index.md +26 -0
- package/package.json +3 -3
- package/src/config.ts +8 -0
- package/src/install/ArchInstallCommand.ts +46 -3
- package/src/install/guidelines.ts +84 -0
- package/src/install/shape.ts +177 -0
- package/src/install/skills.ts +271 -0
- package/src/provider/ArchProvider.ts +148 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ServiceProvider } from "@zerotal/core";
|
|
2
2
|
import type { AppEnvironment, DoctorCheck } from "@zerotal/core";
|
|
3
|
+
import type { ProjectShape } from "../install/shape.ts";
|
|
3
4
|
import { SERVER_ENTRY_PATH } from "../install/mcpConfig.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -96,6 +97,153 @@ export class ArchProvider extends ServiceProvider {
|
|
|
96
97
|
}
|
|
97
98
|
},
|
|
98
99
|
},
|
|
100
|
+
{
|
|
101
|
+
id: "arch-agents-current",
|
|
102
|
+
label: "Agent instructions",
|
|
103
|
+
run: async () => agentsFileCheck(process.cwd()),
|
|
104
|
+
},
|
|
99
105
|
];
|
|
100
106
|
}
|
|
101
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Is the generated block still true?
|
|
111
|
+
*
|
|
112
|
+
* It was a description of the framework, which changed about as often as the
|
|
113
|
+
* framework did. It is now also a description of *this project* — which packages
|
|
114
|
+
* are installed, who owns the schema, which strictness flags are on — and every
|
|
115
|
+
* one of those moves without anyone thinking about `AGENTS.md`. Add a migration
|
|
116
|
+
* directory, turn `synchronize` off, install a package, upgrade the framework:
|
|
117
|
+
* the file still reads as current and is quietly describing the app you had.
|
|
118
|
+
*
|
|
119
|
+
* That is worse than having no file. Guidance nobody wrote is obviously absent;
|
|
120
|
+
* guidance that is confidently out of date gets followed.
|
|
121
|
+
*
|
|
122
|
+
* A warning rather than a failure: a stale instruction file misleads a person or
|
|
123
|
+
* an agent, and does not stop the application working. `doctor` earns the right
|
|
124
|
+
* to gate a deploy by failing only for things that would break one.
|
|
125
|
+
*
|
|
126
|
+
* Takes its root rather than reading `process.cwd()`, so it is testable without
|
|
127
|
+
* moving the process into a fixture.
|
|
128
|
+
*
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
export async function agentsFileCheck(
|
|
132
|
+
root: string,
|
|
133
|
+
): Promise<{ status: "ok" | "warn"; message: string; fix?: string }> {
|
|
134
|
+
const file = Bun.file(`${root}/AGENTS.md`);
|
|
135
|
+
if (!(await file.exists())) {
|
|
136
|
+
return {
|
|
137
|
+
status: "warn",
|
|
138
|
+
message:
|
|
139
|
+
"No AGENTS.md, so an agent working here has no instructions and none of the " +
|
|
140
|
+
"project-specific facts the MCP tools cannot infer.",
|
|
141
|
+
fix: "bun zt arch:install",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const [{ buildGuidelines }, { detectShape }, { installedPackages }, markers] = await Promise.all([
|
|
146
|
+
import("../install/guidelines.ts"),
|
|
147
|
+
import("../install/shape.ts"),
|
|
148
|
+
import("../probe/topics.ts"),
|
|
149
|
+
import("../install/markers.ts"),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
const current = _blockOf(await file.text(), markers.BLOCK_START, markers.BLOCK_END);
|
|
153
|
+
if (current === undefined) {
|
|
154
|
+
return {
|
|
155
|
+
status: "warn",
|
|
156
|
+
message:
|
|
157
|
+
"AGENTS.md has no generated block, so nothing here describes the framework or how " +
|
|
158
|
+
"this app is set up.",
|
|
159
|
+
fix: "bun zt arch:install",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const packages = (await installedPackages(root)).map((pkg) => pkg.name);
|
|
164
|
+
const shape = await detectShape(root);
|
|
165
|
+
const expected = buildGuidelines({
|
|
166
|
+
packages,
|
|
167
|
+
serverName: await _serverName(root),
|
|
168
|
+
shape,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (current.trim() !== expected.trim()) {
|
|
172
|
+
return {
|
|
173
|
+
status: "warn",
|
|
174
|
+
message:
|
|
175
|
+
"AGENTS.md describes a different project than this one — its packages, its setup or " +
|
|
176
|
+
"its framework version have moved since it was written.",
|
|
177
|
+
fix: "bun zt arch:update",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Skills rot the same way and are easier to miss: nothing reads them until an
|
|
182
|
+
// agent decides one is relevant, and by then it is being followed.
|
|
183
|
+
const stale = await _staleSkills(root, packages, shape);
|
|
184
|
+
if (stale.length > 0) {
|
|
185
|
+
return {
|
|
186
|
+
status: "warn",
|
|
187
|
+
message: `AGENTS.md is current, but ${stale.length} skill file(s) are not: ${stale.join(", ")}.`,
|
|
188
|
+
fix: "bun zt arch:update",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { status: "ok", message: "AGENTS.md and skills match this project" };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Skill files that are missing, or generated and no longer what they would be. */
|
|
196
|
+
async function _staleSkills(
|
|
197
|
+
root: string,
|
|
198
|
+
packages: string[],
|
|
199
|
+
shape: ProjectShape,
|
|
200
|
+
): Promise<string[]> {
|
|
201
|
+
const { selectSkills, renderSkill, skillPaths, SKILL_MARKER } =
|
|
202
|
+
await import("../install/skills.ts");
|
|
203
|
+
const { detectAgents } = await import("../install/detect.ts");
|
|
204
|
+
const { agents } = await detectAgents(root);
|
|
205
|
+
|
|
206
|
+
const stale: string[] = [];
|
|
207
|
+
for (const skill of selectSkills(packages, shape)) {
|
|
208
|
+
for (const path of skillPaths(skill.name, agents)) {
|
|
209
|
+
const file = Bun.file(`${root}/${path}`);
|
|
210
|
+
if (!(await file.exists())) {
|
|
211
|
+
stale.push(path);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const text = await file.text();
|
|
215
|
+
// A file somebody took ownership of is theirs, current or not.
|
|
216
|
+
if (!text.includes(SKILL_MARKER)) continue;
|
|
217
|
+
if (text !== renderSkill(skill, shape)) stale.push(path);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return stale;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The generated body, unfenced, or `undefined` when there is no block. */
|
|
224
|
+
function _blockOf(text: string, start: string, end: string): string | undefined {
|
|
225
|
+
const from = text.indexOf(start);
|
|
226
|
+
const to = text.indexOf(end);
|
|
227
|
+
if (from === -1 || to === -1 || to < from) return undefined;
|
|
228
|
+
return text.slice(from + start.length, to);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The key the server is registered under, read back from `.mcp.json`.
|
|
233
|
+
*
|
|
234
|
+
* The block names it, so comparing against a guessed name would report every
|
|
235
|
+
* project that renamed its server as permanently stale. Read rather than
|
|
236
|
+
* `require`d, which would cache the first answer and miss a rename.
|
|
237
|
+
*/
|
|
238
|
+
async function _serverName(root: string): Promise<string> {
|
|
239
|
+
try {
|
|
240
|
+
const document = (await Bun.file(`${root}/.mcp.json`).json()) as {
|
|
241
|
+
mcpServers?: Record<string, unknown>;
|
|
242
|
+
servers?: Record<string, unknown>;
|
|
243
|
+
};
|
|
244
|
+
const names = Object.keys(document.mcpServers ?? document.servers ?? {});
|
|
245
|
+
return names[0] ?? "zerotal";
|
|
246
|
+
} catch {
|
|
247
|
+
return "zerotal";
|
|
248
|
+
}
|
|
249
|
+
}
|