synorch 0.1.0 → 0.3.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 +72 -22
- package/LICENSE +21 -21
- package/README.md +346 -56
- package/dist/application/doctor-service.d.ts +16 -0
- package/dist/application/doctor-service.d.ts.map +1 -1
- package/dist/application/doctor-service.js +230 -32
- package/dist/application/doctor-service.js.map +1 -1
- package/dist/application/generated-skill-doctor.d.ts +9 -0
- package/dist/application/generated-skill-doctor.d.ts.map +1 -0
- package/dist/application/generated-skill-doctor.js +433 -0
- package/dist/application/generated-skill-doctor.js.map +1 -0
- package/dist/application/project-discovery.d.ts +12 -0
- package/dist/application/project-discovery.d.ts.map +1 -1
- package/dist/application/project-discovery.js +65 -2
- package/dist/application/project-discovery.js.map +1 -1
- package/dist/application/safe-path.d.ts +19 -0
- package/dist/application/safe-path.d.ts.map +1 -0
- package/dist/application/safe-path.js +35 -0
- package/dist/application/safe-path.js.map +1 -0
- package/dist/application/size-ceiling.d.ts +15 -0
- package/dist/application/size-ceiling.d.ts.map +1 -0
- package/dist/application/size-ceiling.js +32 -0
- package/dist/application/size-ceiling.js.map +1 -0
- package/dist/application/structure-service.d.ts.map +1 -1
- package/dist/application/structure-service.js +10 -1
- package/dist/application/structure-service.js.map +1 -1
- package/dist/cli.js +27 -18
- package/dist/cli.js.map +1 -1
- package/dist/domain/canonical-contracts.d.ts +69 -0
- package/dist/domain/canonical-contracts.d.ts.map +1 -0
- package/dist/domain/canonical-contracts.js +92 -0
- package/dist/domain/canonical-contracts.js.map +1 -0
- package/dist/domain/generated-skill.d.ts +55 -0
- package/dist/domain/generated-skill.d.ts.map +1 -0
- package/dist/domain/generated-skill.js +90 -0
- package/dist/domain/generated-skill.js.map +1 -0
- package/dist/domain/generation.d.ts +15 -2
- package/dist/domain/generation.d.ts.map +1 -1
- package/dist/domain/observation-ledger.d.ts +118 -0
- package/dist/domain/observation-ledger.d.ts.map +1 -0
- package/dist/domain/observation-ledger.js +183 -0
- package/dist/domain/observation-ledger.js.map +1 -0
- package/dist/domain/product.d.ts +1 -1
- package/dist/domain/product.js +1 -1
- package/dist/domain/relative-path.d.ts +21 -0
- package/dist/domain/relative-path.d.ts.map +1 -0
- package/dist/domain/relative-path.js +43 -0
- package/dist/domain/relative-path.js.map +1 -0
- package/dist/domain/skill-packs.d.ts.map +1 -1
- package/dist/domain/skill-packs.js +2 -0
- package/dist/domain/skill-packs.js.map +1 -1
- package/dist/domain/skill-sources.d.ts.map +1 -1
- package/dist/domain/skill-sources.js +0 -1
- package/dist/domain/skill-sources.js.map +1 -1
- package/dist/domain/zod-issues.d.ts +21 -0
- package/dist/domain/zod-issues.d.ts.map +1 -0
- package/dist/domain/zod-issues.js +27 -0
- package/dist/domain/zod-issues.js.map +1 -0
- package/dist/infrastructure/bundled-skill-library.d.ts.map +1 -1
- package/dist/infrastructure/bundled-skill-library.js +2 -0
- package/dist/infrastructure/bundled-skill-library.js.map +1 -1
- package/dist/infrastructure/frontmatter.d.ts +25 -0
- package/dist/infrastructure/frontmatter.d.ts.map +1 -0
- package/dist/infrastructure/frontmatter.js +75 -0
- package/dist/infrastructure/frontmatter.js.map +1 -0
- package/dist/templates/agent-manifests.d.ts +19 -0
- package/dist/templates/agent-manifests.d.ts.map +1 -0
- package/dist/templates/agent-manifests.js +302 -0
- package/dist/templates/agent-manifests.js.map +1 -0
- package/dist/templates/base-skills.d.ts +23 -0
- package/dist/templates/base-skills.d.ts.map +1 -0
- package/dist/templates/base-skills.js +477 -0
- package/dist/templates/base-skills.js.map +1 -0
- package/dist/templates/skill-creator-skill.d.ts +23 -0
- package/dist/templates/skill-creator-skill.d.ts.map +1 -0
- package/dist/templates/skill-creator-skill.js +235 -0
- package/dist/templates/skill-creator-skill.js.map +1 -0
- package/dist/templates/skill-references.d.ts +13 -0
- package/dist/templates/skill-references.d.ts.map +1 -0
- package/dist/templates/skill-references.js +280 -0
- package/dist/templates/skill-references.js.map +1 -0
- package/dist/templates/structure-templates.d.ts.map +1 -1
- package/dist/templates/structure-templates.js +280 -349
- package/dist/templates/structure-templates.js.map +1 -1
- package/dist/templates/task-conductor-skill.d.ts +2 -0
- package/dist/templates/task-conductor-skill.d.ts.map +1 -0
- package/dist/templates/task-conductor-skill.js +78 -0
- package/dist/templates/task-conductor-skill.js.map +1 -0
- package/package.json +59 -59
- package/skill-sources/ingenium/skills/task-conductor/SKILL.md +0 -158
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { GENERATED_SKILL_ACTIVE_BUDGET, GENERATED_SKILL_DIRECTORY, GENERATED_SKILL_MAX_BYTES, generatedSkillFrontmatterSchema, } from "../domain/generated-skill.js";
|
|
4
|
+
import { DIGEST_MIN_LENGTH, OBSERVATION_LEDGER_PATH, observationLedgerSchema, } from "../domain/observation-ledger.js";
|
|
5
|
+
import { isSafeDescendantPath } from "../domain/relative-path.js";
|
|
6
|
+
import { formatZodIssues, partitionZodIssues } from "../domain/zod-issues.js";
|
|
7
|
+
import { parseFrontmatter, splitMarkdownSections, normalizeSectionName, } from "../infrastructure/frontmatter.js";
|
|
8
|
+
import { parseYaml } from "../infrastructure/serialization.js";
|
|
9
|
+
import { resolveSafeRelativePath } from "./safe-path.js";
|
|
10
|
+
import { checkByteCeiling } from "./size-ceiling.js";
|
|
11
|
+
/**
|
|
12
|
+
* Validate the generated project-skill namespace: the ledger itself, the frontmatter contract of
|
|
13
|
+
* every `SKILL.md` under `.ai/skills/project/**`, the evidence behind it, and the shape heuristics.
|
|
14
|
+
* Contract violations are errors; heuristics and staleness are warnings, per design D13.
|
|
15
|
+
*/
|
|
16
|
+
export async function diagnoseGeneratedSkills(fileSystem, rootDirectory) {
|
|
17
|
+
const root = path.resolve(rootDirectory);
|
|
18
|
+
const diagnostics = [];
|
|
19
|
+
const skillDirectories = await listSkillDirectories(fileSystem, root);
|
|
20
|
+
const ledger = await readLedger(fileSystem, root, skillDirectories.length > 0, diagnostics);
|
|
21
|
+
const knownTaskIds = collectConfirmingTaskIds(ledger);
|
|
22
|
+
let activeSkills = 0;
|
|
23
|
+
const readableSkillIds = new Set();
|
|
24
|
+
for (const skillId of skillDirectories) {
|
|
25
|
+
const relativePath = `${GENERATED_SKILL_DIRECTORY}/${skillId}/SKILL.md`;
|
|
26
|
+
const absolutePath = path.join(root, GENERATED_SKILL_DIRECTORY, skillId, "SKILL.md");
|
|
27
|
+
if (await isRetiredSkill(fileSystem, root, skillId))
|
|
28
|
+
readableSkillIds.add(skillId);
|
|
29
|
+
if (!(await isReadableFileWithinRoot(fileSystem, root, absolutePath)))
|
|
30
|
+
continue;
|
|
31
|
+
readableSkillIds.add(skillId);
|
|
32
|
+
const content = await fileSystem.readText(absolutePath);
|
|
33
|
+
if (Buffer.byteLength(content, "utf8") > GENERATED_SKILL_MAX_BYTES) {
|
|
34
|
+
diagnostics.push({
|
|
35
|
+
severity: "error",
|
|
36
|
+
code: "generated.size-exceeded",
|
|
37
|
+
message: `Generated skill exceeds the ${GENERATED_SKILL_MAX_BYTES}-byte ceiling: ` +
|
|
38
|
+
`${Buffer.byteLength(content, "utf8")} bytes.`,
|
|
39
|
+
path: relativePath,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const document = readSkillDocument(content, relativePath, diagnostics);
|
|
43
|
+
if (document === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
const parsed = validateFrontmatter(document.data, skillId, relativePath, diagnostics);
|
|
46
|
+
reportShapeHeuristics(document.body, parsed, relativePath, diagnostics);
|
|
47
|
+
if (parsed === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
if (parsed.status === "active")
|
|
50
|
+
activeSkills += 1;
|
|
51
|
+
await validateEvidence(fileSystem, root, parsed, relativePath, diagnostics);
|
|
52
|
+
await validateReferences(fileSystem, root, skillId, parsed, relativePath, diagnostics);
|
|
53
|
+
if (ledger !== undefined) {
|
|
54
|
+
reportUnknownConfirmations(parsed, knownTaskIds, relativePath, diagnostics);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
reportBrokenPromotionLinks(ledger, readableSkillIds, diagnostics);
|
|
58
|
+
if (activeSkills > GENERATED_SKILL_ACTIVE_BUDGET) {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
severity: "error",
|
|
61
|
+
code: "generated.budget-exceeded",
|
|
62
|
+
message: `Active project skills (${activeSkills}) exceed the budget of ` +
|
|
63
|
+
`${GENERATED_SKILL_ACTIVE_BUDGET}. Retire one before promoting another.`,
|
|
64
|
+
path: GENERATED_SKILL_DIRECTORY,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return diagnostics;
|
|
68
|
+
}
|
|
69
|
+
/** Directory names under `.ai/skills/project`, sorted, so diagnostics keep a stable order. */
|
|
70
|
+
async function listSkillDirectories(fileSystem, root) {
|
|
71
|
+
const directory = path.join(root, GENERATED_SKILL_DIRECTORY);
|
|
72
|
+
if (!(await fileSystem.exists(directory)) ||
|
|
73
|
+
!(await fileSystem.isDirectory(directory)) ||
|
|
74
|
+
!(await isPathWithinRoot(fileSystem, root, directory))) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
return [...(await fileSystem.list(directory))]
|
|
78
|
+
.filter((entry) => entry.isDirectory)
|
|
79
|
+
.map((entry) => entry.name)
|
|
80
|
+
.sort((left, right) => left.localeCompare(right));
|
|
81
|
+
}
|
|
82
|
+
async function readLedger(fileSystem, root, skillsExist, diagnostics) {
|
|
83
|
+
const absolutePath = path.join(root, OBSERVATION_LEDGER_PATH);
|
|
84
|
+
if (!(await fileSystem.exists(absolutePath))) {
|
|
85
|
+
if (skillsExist) {
|
|
86
|
+
diagnostics.push({
|
|
87
|
+
severity: "error",
|
|
88
|
+
code: "generated.ledger-invalid",
|
|
89
|
+
message: "Observation ledger is missing, so generated skill confirmations cannot be verified.",
|
|
90
|
+
path: OBSERVATION_LEDGER_PATH,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = parseYaml(await fileSystem.readText(absolutePath));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
diagnostics.push({
|
|
101
|
+
severity: "error",
|
|
102
|
+
code: "generated.ledger-invalid",
|
|
103
|
+
message: error instanceof Error ? error.message : String(error),
|
|
104
|
+
path: OBSERVATION_LEDGER_PATH,
|
|
105
|
+
});
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const result = observationLedgerSchema.safeParse(parsed);
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
diagnostics.push({
|
|
111
|
+
severity: "error",
|
|
112
|
+
code: "generated.ledger-invalid",
|
|
113
|
+
message: `Observation ledger is invalid: ${formatZodIssues(result.error)}`,
|
|
114
|
+
path: OBSERVATION_LEDGER_PATH,
|
|
115
|
+
});
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
return result.data;
|
|
119
|
+
}
|
|
120
|
+
/** A retired skill keeps its directory but lives in `RETIRED.md`; the promotion link still holds. */
|
|
121
|
+
async function isRetiredSkill(fileSystem, root, skillId) {
|
|
122
|
+
const absolutePath = path.join(root, GENERATED_SKILL_DIRECTORY, skillId, "RETIRED.md");
|
|
123
|
+
return isReadableFileWithinRoot(fileSystem, root, absolutePath);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The ledger half of the activation record must point at something real.
|
|
127
|
+
*
|
|
128
|
+
* Only this direction is enforced. The reverse — every `status: active` skill having a
|
|
129
|
+
* `promoted_to` observation that names it — would require an observation id and a skill id to
|
|
130
|
+
* be the same string, which nothing in the design states, and would reject a project skill a
|
|
131
|
+
* user placed by hand. The confirming task ids already tie a generated skill back to the
|
|
132
|
+
* ledger through `generated.unknown-confirmation`.
|
|
133
|
+
*/
|
|
134
|
+
function reportBrokenPromotionLinks(ledger, skillIds, diagnostics) {
|
|
135
|
+
for (const observation of ledger?.observations ?? []) {
|
|
136
|
+
if (observation.status !== "promoted")
|
|
137
|
+
continue;
|
|
138
|
+
const promotedTo = observation.promoted_to;
|
|
139
|
+
if (promotedTo === undefined || skillIds.has(promotedTo))
|
|
140
|
+
continue;
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
severity: "error",
|
|
143
|
+
code: "generated.broken-promotion-link",
|
|
144
|
+
message: `Observation '${observation.id}' is promoted to '${promotedTo}', but ` +
|
|
145
|
+
`${GENERATED_SKILL_DIRECTORY}/${promotedTo}/SKILL.md does not exist. Restore the skill ` +
|
|
146
|
+
"or correct the ledger; a promotion that names nothing is not a record.",
|
|
147
|
+
path: OBSERVATION_LEDGER_PATH,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function collectConfirmingTaskIds(ledger) {
|
|
152
|
+
const taskIds = new Set();
|
|
153
|
+
for (const observation of ledger?.observations ?? []) {
|
|
154
|
+
for (const taskId of observation.confirmed_by)
|
|
155
|
+
taskIds.add(taskId);
|
|
156
|
+
}
|
|
157
|
+
return taskIds;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Reads the shared canonical frontmatter block. `missing` and `malformed` keep the dedicated
|
|
161
|
+
* codes the generated namespace already reports, so the two contracts share one parser without
|
|
162
|
+
* sharing a diagnostic vocabulary.
|
|
163
|
+
*/
|
|
164
|
+
function readSkillDocument(content, relativePath, diagnostics) {
|
|
165
|
+
const result = parseFrontmatter(content);
|
|
166
|
+
if (result.kind === "missing") {
|
|
167
|
+
diagnostics.push({
|
|
168
|
+
severity: "error",
|
|
169
|
+
code: "generated.frontmatter-missing",
|
|
170
|
+
message: "Generated skill has no YAML frontmatter block fenced by '---'.",
|
|
171
|
+
path: relativePath,
|
|
172
|
+
});
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
if (result.kind === "malformed") {
|
|
176
|
+
diagnostics.push({
|
|
177
|
+
severity: "error",
|
|
178
|
+
code: "generated.frontmatter-invalid",
|
|
179
|
+
message: result.message,
|
|
180
|
+
path: relativePath,
|
|
181
|
+
});
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Dedicated codes come first so that the priority ceiling, the evidence requirement and an
|
|
188
|
+
* unsafe reference path are reported as themselves rather than as a generic schema failure.
|
|
189
|
+
* Each field that got a dedicated diagnostic is then excluded from `generated.contract-invalid`,
|
|
190
|
+
* so one defect is never reported twice, and the generic code is skipped when nothing is left.
|
|
191
|
+
*/
|
|
192
|
+
function validateFrontmatter(frontmatter, skillId, relativePath, diagnostics) {
|
|
193
|
+
const reportedFields = [];
|
|
194
|
+
const name = frontmatter["name"];
|
|
195
|
+
if (typeof name === "string" && name !== skillId) {
|
|
196
|
+
reportedFields.push("name");
|
|
197
|
+
diagnostics.push({
|
|
198
|
+
severity: "error",
|
|
199
|
+
code: "generated.name-directory-mismatch",
|
|
200
|
+
message: `Generated skill name '${name}' must match its project-skill directory '${skillId}'.`,
|
|
201
|
+
path: relativePath,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const priority = frontmatter["priority"];
|
|
205
|
+
if (priority !== "skill") {
|
|
206
|
+
reportedFields.push("priority");
|
|
207
|
+
diagnostics.push({
|
|
208
|
+
severity: "error",
|
|
209
|
+
code: "generated.priority-ceiling",
|
|
210
|
+
message: "A generated skill must declare 'priority: skill'; it can never hold constitutional " +
|
|
211
|
+
`or protocol priority. Received: ${describeValue(priority)}.`,
|
|
212
|
+
path: relativePath,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (!hasCompleteEvidence(frontmatter["evidence"])) {
|
|
216
|
+
reportedFields.push("evidence");
|
|
217
|
+
diagnostics.push({
|
|
218
|
+
severity: "error",
|
|
219
|
+
code: "generated.missing-evidence",
|
|
220
|
+
message: "origin: generated requires a non-empty evidence list where every entry carries a " +
|
|
221
|
+
"claim, a source and a digest.",
|
|
222
|
+
path: relativePath,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
for (const reference of unsafeReferencePaths(frontmatter["references"])) {
|
|
226
|
+
reportedFields.push("references");
|
|
227
|
+
diagnostics.push({
|
|
228
|
+
severity: "error",
|
|
229
|
+
code: "generated.unsafe-reference-path",
|
|
230
|
+
message: "A reference must be a relative path inside the skill's own directory; an absolute " +
|
|
231
|
+
`path, a drive letter, a UNC root or a '..' segment is rejected: ${reference}`,
|
|
232
|
+
path: relativePath,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const result = generatedSkillFrontmatterSchema.safeParse(frontmatter);
|
|
236
|
+
if (!result.success) {
|
|
237
|
+
const { remaining } = partitionZodIssues(result.error, reportedFields);
|
|
238
|
+
if (remaining.length > 0) {
|
|
239
|
+
diagnostics.push({
|
|
240
|
+
severity: "error",
|
|
241
|
+
code: "generated.contract-invalid",
|
|
242
|
+
message: `Generated skill frontmatter is invalid: ${formatZodIssues({ issues: remaining })}`,
|
|
243
|
+
path: relativePath,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
return result.data;
|
|
249
|
+
}
|
|
250
|
+
/** Reference entries the lexical rule rejects, reported before the schema sees them. */
|
|
251
|
+
function unsafeReferencePaths(value) {
|
|
252
|
+
if (!Array.isArray(value))
|
|
253
|
+
return [];
|
|
254
|
+
return value.filter((entry) => typeof entry === "string" && !isSafeDescendantPath(entry));
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The reference half of the contract, mirroring the canonical check in `doctor-service`: a
|
|
258
|
+
* declared reference must exist, stay inside the skill's own directory both lexically and after
|
|
259
|
+
* symbolic links are canonicalized, and respect the reference byte ceiling. The lexical rule
|
|
260
|
+
* already ran in `validateFrontmatter`, so anything reaching here is lexically safe.
|
|
261
|
+
*/
|
|
262
|
+
async function validateReferences(fileSystem, root, skillId, frontmatter, relativePath, diagnostics) {
|
|
263
|
+
const skillDirectory = path.join(root, GENERATED_SKILL_DIRECTORY, skillId);
|
|
264
|
+
for (const reference of frontmatter.references ?? []) {
|
|
265
|
+
const resolved = resolveSafeRelativePath(skillDirectory, reference);
|
|
266
|
+
if (resolved === undefined) {
|
|
267
|
+
diagnostics.push({
|
|
268
|
+
severity: "error",
|
|
269
|
+
code: "generated.unsafe-reference-path",
|
|
270
|
+
message: `Reference escapes the skill directory: ${reference}`,
|
|
271
|
+
path: relativePath,
|
|
272
|
+
});
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!(await fileSystem.exists(resolved.absolute)) ||
|
|
276
|
+
(await fileSystem.isDirectory(resolved.absolute))) {
|
|
277
|
+
diagnostics.push({
|
|
278
|
+
severity: "error",
|
|
279
|
+
code: "generated.missing-reference-file",
|
|
280
|
+
message: `Declared reference is not an existing file: ${reference}`,
|
|
281
|
+
path: relativePath,
|
|
282
|
+
});
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!(await isPathWithinRoot(fileSystem, skillDirectory, resolved.absolute)) ||
|
|
286
|
+
!(await isPathWithinRoot(fileSystem, root, resolved.absolute))) {
|
|
287
|
+
diagnostics.push({
|
|
288
|
+
severity: "error",
|
|
289
|
+
code: "generated.unsafe-reference-path",
|
|
290
|
+
message: `Reference escapes the skill directory through a link: ${reference}`,
|
|
291
|
+
path: relativePath,
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
checkByteCeiling(`${GENERATED_SKILL_DIRECTORY}/${skillId}/${resolved.relative}`, await fileSystem.readText(resolved.absolute), "skillReference", diagnostics, "generated.reference-size");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function hasCompleteEvidence(value) {
|
|
299
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
300
|
+
return false;
|
|
301
|
+
return value.every((entry) => {
|
|
302
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
303
|
+
return false;
|
|
304
|
+
const record = entry;
|
|
305
|
+
return (typeof record["claim"] === "string" &&
|
|
306
|
+
typeof record["source"] === "string" &&
|
|
307
|
+
typeof record["digest"] === "string");
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async function validateEvidence(fileSystem, root, frontmatter, relativePath, diagnostics) {
|
|
311
|
+
for (const evidence of frontmatter.evidence) {
|
|
312
|
+
const resolved = resolveSafeRelativePath(root, evidence.source)?.absolute;
|
|
313
|
+
if (resolved === undefined || !(await isPathWithinRoot(fileSystem, root, resolved))) {
|
|
314
|
+
diagnostics.push({
|
|
315
|
+
severity: "error",
|
|
316
|
+
code: "generated.unsafe-source",
|
|
317
|
+
message: `Evidence source must stay inside the target root: ${evidence.source}`,
|
|
318
|
+
path: relativePath,
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (!(await fileSystem.exists(resolved)) ||
|
|
323
|
+
(await fileSystem.isDirectory(resolved))) {
|
|
324
|
+
diagnostics.push({
|
|
325
|
+
severity: "error",
|
|
326
|
+
code: "generated.missing-source",
|
|
327
|
+
message: `Evidence source is not an existing regular file: ${evidence.source}`,
|
|
328
|
+
path: relativePath,
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const current = digestOf(await fileSystem.readText(resolved));
|
|
333
|
+
if (!matchesRecordedDigest(evidence.digest, current)) {
|
|
334
|
+
diagnostics.push({
|
|
335
|
+
severity: "warning",
|
|
336
|
+
code: "generated.stale-evidence",
|
|
337
|
+
message: `Evidence source changed since the skill was verified: ${evidence.source} is now ` +
|
|
338
|
+
`${current}. Re-verify the claim or retire the skill.`,
|
|
339
|
+
path: relativePath,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function reportUnknownConfirmations(frontmatter, knownTaskIds, relativePath, diagnostics) {
|
|
345
|
+
for (const taskId of frontmatter.confirmed_by) {
|
|
346
|
+
if (knownTaskIds.has(taskId))
|
|
347
|
+
continue;
|
|
348
|
+
diagnostics.push({
|
|
349
|
+
severity: "error",
|
|
350
|
+
code: "generated.unknown-confirmation",
|
|
351
|
+
message: `Confirming task id '${taskId}' does not appear in the observation ledger.`,
|
|
352
|
+
path: relativePath,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const PAST_TENSE_MARKERS = /\b(?:was|were|had|did|failed|crashed|broke|discovered|noticed|turned out|we (?:saw|hit|found|tried|ran))\b/gi;
|
|
357
|
+
const TASK_REFERENCE = /\btask-\d+\b/i;
|
|
358
|
+
const DATE_REFERENCE = /\b\d{4}-\d{2}-\d{2}\b/;
|
|
359
|
+
const PATH_LIKE_CODE_SPAN = /`([^`\n]+)`/g;
|
|
360
|
+
/** The section that states when the skill applies; fenced examples of it do not count. */
|
|
361
|
+
const ACTIVATION_SECTION = "When this applies";
|
|
362
|
+
/** Taste-level signals from design §7. They warn; they never block. */
|
|
363
|
+
function reportShapeHeuristics(body, frontmatter, relativePath, diagnostics) {
|
|
364
|
+
if (!splitMarkdownSections(body).has(normalizeSectionName(ACTIVATION_SECTION))) {
|
|
365
|
+
diagnostics.push({
|
|
366
|
+
severity: "warning",
|
|
367
|
+
code: "shape.no-trigger",
|
|
368
|
+
message: "No explicit activation condition: add a 'When this applies' section so the skill can " +
|
|
369
|
+
"be matched without reading it.",
|
|
370
|
+
path: relativePath,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const pastTenseMarkers = body.match(PAST_TENSE_MARKERS)?.length ?? 0;
|
|
374
|
+
if (pastTenseMarkers >= 3 && (TASK_REFERENCE.test(body) || DATE_REFERENCE.test(body))) {
|
|
375
|
+
diagnostics.push({
|
|
376
|
+
severity: "warning",
|
|
377
|
+
code: "shape.incident-log-shape",
|
|
378
|
+
message: "The body reads as a narrated past incident (past-tense narration plus a task id or " +
|
|
379
|
+
"date) instead of a repeatable procedure.",
|
|
380
|
+
path: relativePath,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
if (frontmatter === undefined)
|
|
384
|
+
return;
|
|
385
|
+
const sources = new Set(frontmatter.evidence.map((evidence) => evidence.source));
|
|
386
|
+
const unsourced = [...body.matchAll(PATH_LIKE_CODE_SPAN)]
|
|
387
|
+
.map((match) => match[1] ?? "")
|
|
388
|
+
.filter((value) => isPathLike(value) && !sources.has(value))
|
|
389
|
+
.sort((left, right) => left.localeCompare(right));
|
|
390
|
+
const firstUnsourced = unsourced[0];
|
|
391
|
+
if (firstUnsourced !== undefined) {
|
|
392
|
+
diagnostics.push({
|
|
393
|
+
severity: "warning",
|
|
394
|
+
code: "shape.unsourced-claim",
|
|
395
|
+
message: `The body cites '${firstUnsourced}', which no evidence entry backs. Add it to evidence ` +
|
|
396
|
+
"with a digest or drop the claim.",
|
|
397
|
+
path: relativePath,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function isPathLike(value) {
|
|
402
|
+
return /^[\w.@/-]+$/.test(value) && value.includes("/") && !value.endsWith("/");
|
|
403
|
+
}
|
|
404
|
+
/** Digests are taken over line-ending-normalized text so they are stable across platforms. */
|
|
405
|
+
function digestOf(content) {
|
|
406
|
+
return `sha256:${createHash("sha256").update(content.replaceAll("\r\n", "\n"), "utf8").digest("hex")}`;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* A recorded digest may be truncated, so a prefix of the current digest is a match — but only a
|
|
410
|
+
* prefix long enough to mean something. Anything shorter than the schema floor is treated as no
|
|
411
|
+
* match, so a deliberately short digest cannot silence the staleness check.
|
|
412
|
+
*/
|
|
413
|
+
function matchesRecordedDigest(recorded, current) {
|
|
414
|
+
return recorded.length >= DIGEST_MIN_LENGTH && current.startsWith(recorded);
|
|
415
|
+
}
|
|
416
|
+
function describeValue(value) {
|
|
417
|
+
return typeof value === "string" ? `'${value}'` : JSON.stringify(value) ?? "undefined";
|
|
418
|
+
}
|
|
419
|
+
async function isReadableFileWithinRoot(fileSystem, root, absolutePath) {
|
|
420
|
+
return ((await fileSystem.exists(absolutePath)) &&
|
|
421
|
+
!(await fileSystem.isDirectory(absolutePath)) &&
|
|
422
|
+
(await isPathWithinRoot(fileSystem, root, absolutePath)));
|
|
423
|
+
}
|
|
424
|
+
async function isPathWithinRoot(fileSystem, root, absolutePath) {
|
|
425
|
+
try {
|
|
426
|
+
await fileSystem.assertPathWithinRoot(root, absolutePath);
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
//# sourceMappingURL=generated-skill-doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated-skill-doctor.js","sourceRoot":"","sources":["../../src/application/generated-skill-doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,8 @@ import type { FileSystem } from "../infrastructure/file-system.ts";
|
|
|
3
3
|
export interface SyncResult {
|
|
4
4
|
readonly projects: readonly ProjectRecord[];
|
|
5
5
|
readonly writtenFiles: readonly string[];
|
|
6
|
+
/** Observations removed from the ledger because they expired; 0 when there is no ledger. */
|
|
7
|
+
readonly prunedObservations: number;
|
|
6
8
|
}
|
|
7
9
|
export interface SyncOptions {
|
|
8
10
|
readonly force?: boolean;
|
|
@@ -11,6 +13,11 @@ export declare class ProjectDiscoveryService {
|
|
|
11
13
|
private readonly fileSystem;
|
|
12
14
|
constructor(fileSystem: FileSystem);
|
|
13
15
|
sync(targetDirectory: string, options?: SyncOptions): Promise<SyncResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Prune expired observations from the ledger. The ledger is optional: a project that has never
|
|
18
|
+
* recorded one syncs exactly as before. Returns undefined when there is nothing to rewrite.
|
|
19
|
+
*/
|
|
20
|
+
private pruneObservationLedger;
|
|
14
21
|
private readPreviousDetectedAt;
|
|
15
22
|
private findCandidates;
|
|
16
23
|
private isProject;
|
|
@@ -20,4 +27,9 @@ export declare class ProjectDiscoveryService {
|
|
|
20
27
|
private findDiscoveryFileNames;
|
|
21
28
|
private inspectModule;
|
|
22
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a sync-generated path inside the root. Exported so the protected-namespace guard can
|
|
32
|
+
* be asserted directly rather than only through a full `sync`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveWithinRoot(root: string, relativePath: string): string;
|
|
23
35
|
//# sourceMappingURL=project-discovery.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-discovery.d.ts","sourceRoot":"","sources":["../../src/application/project-discovery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,aAAa,EAId,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"project-discovery.d.ts","sourceRoot":"","sources":["../../src/application/project-discovery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,aAAa,EAId,MAAM,qBAAqB,CAAC;AAS7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAkDnE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,4FAA4F;IAC5F,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAcD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAYD,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;gBAErB,UAAU,EAAE,UAAU;IAI5B,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IAuL1F;;;OAGG;YACW,sBAAsB;YA+BtB,sBAAsB;YA4BtB,cAAc;YAwBd,SAAS;YAOT,cAAc;YAsBd,eAAe;YA0Df,2BAA2B;YAoC3B,sBAAsB;YAQtB,aAAa;CA+H5B;AA6WD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAW5E"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { workspaceSchema } from "../domain/config.js";
|
|
3
3
|
import { CliError } from "../domain/errors.js";
|
|
4
|
+
import { OBSERVATION_LEDGER_PATH, observationLedgerSchema, pruneExpiredObservations, } from "../domain/observation-ledger.js";
|
|
5
|
+
import { formatZodIssues } from "../domain/zod-issues.js";
|
|
4
6
|
import { loadBundledSkillPool } from "../infrastructure/bundled-skill-library.js";
|
|
5
7
|
import { parseYaml, stringifyYaml } from "../infrastructure/serialization.js";
|
|
6
8
|
import { resolveSkillPacks } from "./skill-resolver.js";
|
|
@@ -48,6 +50,12 @@ const SOURCE_TREE_DIRECTORIES = new Set([
|
|
|
48
50
|
const MAX_DISCOVERY_DEPTH = 6;
|
|
49
51
|
const MAX_DISCOVERED_DIRECTORIES = 2_000;
|
|
50
52
|
const MAX_SOURCE_TREE_DIRECTORIES = 2_000;
|
|
53
|
+
/**
|
|
54
|
+
* Paths `sync` must never create, overwrite or delete, with or without `--force`. Generated
|
|
55
|
+
* project skills are authored by a worker and approved by the user, so they are not generated
|
|
56
|
+
* output and no regeneration may touch them.
|
|
57
|
+
*/
|
|
58
|
+
const PROTECTED_WRITE_PREFIXES = [".ai/skills/project"];
|
|
51
59
|
export class ProjectDiscoveryService {
|
|
52
60
|
fileSystem;
|
|
53
61
|
constructor(fileSystem) {
|
|
@@ -139,8 +147,12 @@ export class ProjectDiscoveryService {
|
|
|
139
147
|
await resolveSafeGeneratedPath(this.fileSystem, root, prepared.record.skill_registry);
|
|
140
148
|
}
|
|
141
149
|
await resolveSafeGeneratedPath(this.fileSystem, root, ".ai/workspace.yaml");
|
|
150
|
+
const ledgerPrune = await this.pruneObservationLedger(root, new Date());
|
|
142
151
|
const writtenFiles = [];
|
|
143
152
|
const writtenFileSet = new Set();
|
|
153
|
+
if (ledgerPrune !== undefined) {
|
|
154
|
+
await writeIfChanged(this.fileSystem, root, resolveWithinRoot(root, OBSERVATION_LEDGER_PATH), ledgerPrune.content, OBSERVATION_LEDGER_PATH, writtenFiles, writtenFileSet);
|
|
155
|
+
}
|
|
144
156
|
for (const skill of skillWrites) {
|
|
145
157
|
await assertSafeGeneratedPath(this.fileSystem, root, skill.absolutePath, skill.relativePath);
|
|
146
158
|
await this.fileSystem.writeText(skill.absolutePath, skill.content);
|
|
@@ -164,7 +176,36 @@ export class ProjectDiscoveryService {
|
|
|
164
176
|
for (const prepared of preparedProjects) {
|
|
165
177
|
await writeIfChanged(this.fileSystem, root, resolveWithinRoot(root, prepared.record.skill_registry), stringifyYaml(prepared.registry), prepared.record.skill_registry, writtenFiles, writtenFileSet);
|
|
166
178
|
}
|
|
167
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
projects,
|
|
181
|
+
writtenFiles,
|
|
182
|
+
prunedObservations: ledgerPrune?.expired.length ?? 0,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Prune expired observations from the ledger. The ledger is optional: a project that has never
|
|
187
|
+
* recorded one syncs exactly as before. Returns undefined when there is nothing to rewrite.
|
|
188
|
+
*/
|
|
189
|
+
async pruneObservationLedger(root, now) {
|
|
190
|
+
const absolutePath = await resolveSafeGeneratedPath(this.fileSystem, root, OBSERVATION_LEDGER_PATH);
|
|
191
|
+
if (!(await this.fileSystem.exists(absolutePath)))
|
|
192
|
+
return undefined;
|
|
193
|
+
let parsed;
|
|
194
|
+
try {
|
|
195
|
+
parsed = parseYaml(await this.fileSystem.readText(absolutePath));
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
199
|
+
throw new CliError(`Invalid observation ledger at ${OBSERVATION_LEDGER_PATH}: ${detail}`, 2);
|
|
200
|
+
}
|
|
201
|
+
const ledger = observationLedgerSchema.safeParse(parsed);
|
|
202
|
+
if (!ledger.success) {
|
|
203
|
+
throw new CliError(`Invalid observation ledger at ${OBSERVATION_LEDGER_PATH}: ${formatZodIssues(ledger.error)}`, 2);
|
|
204
|
+
}
|
|
205
|
+
const pruned = pruneExpiredObservations(ledger.data, now);
|
|
206
|
+
if (pruned.expired.length === 0)
|
|
207
|
+
return undefined;
|
|
208
|
+
return { content: stringifyYaml(pruned.ledger), expired: pruned.expired };
|
|
168
209
|
}
|
|
169
210
|
async readPreviousDetectedAt(root, workspace, identity) {
|
|
170
211
|
const entry = workspace.projects.find((project) => project.id === identity.id && project.path === identity.path);
|
|
@@ -687,10 +728,15 @@ function assertUniqueModuleIdentifiers(projectId, modules) {
|
|
|
687
728
|
pathsById.set(module.id, module.path);
|
|
688
729
|
}
|
|
689
730
|
}
|
|
690
|
-
|
|
731
|
+
/**
|
|
732
|
+
* Resolve a sync-generated path inside the root. Exported so the protected-namespace guard can
|
|
733
|
+
* be asserted directly rather than only through a full `sync`.
|
|
734
|
+
*/
|
|
735
|
+
export function resolveWithinRoot(root, relativePath) {
|
|
691
736
|
if (path.isAbsolute(relativePath)) {
|
|
692
737
|
throw new CliError(`Generated path must be relative: ${relativePath}`, 2);
|
|
693
738
|
}
|
|
739
|
+
assertWritablePath(relativePath);
|
|
694
740
|
const resolved = path.resolve(root, relativePath);
|
|
695
741
|
const boundary = path.relative(root, resolved);
|
|
696
742
|
if (boundary === ".." || boundary.startsWith(`..${path.sep}`) || path.isAbsolute(boundary)) {
|
|
@@ -698,6 +744,23 @@ function resolveWithinRoot(root, relativePath) {
|
|
|
698
744
|
}
|
|
699
745
|
return resolved;
|
|
700
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* The single choke point for the generated-skill namespace: every sync write resolves its path
|
|
749
|
+
* here, so no regeneration path — including `--force` — can reach `.ai/skills/project/**`.
|
|
750
|
+
*/
|
|
751
|
+
function assertWritablePath(relativePath) {
|
|
752
|
+
const normalized = relativePath
|
|
753
|
+
.replaceAll("\\", "/")
|
|
754
|
+
.replace(/\/+/g, "/")
|
|
755
|
+
.replace(/^(?:\.\/)+/, "")
|
|
756
|
+
.replace(/\/$/, "")
|
|
757
|
+
.toLowerCase();
|
|
758
|
+
for (const prefix of PROTECTED_WRITE_PREFIXES) {
|
|
759
|
+
if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
|
|
760
|
+
throw new CliError(`Sync must never write inside the generated skill namespace: ${relativePath}`, 2);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
701
764
|
async function resolveSafeGeneratedPath(fileSystem, root, relativePath) {
|
|
702
765
|
const absolutePath = resolveWithinRoot(root, relativePath);
|
|
703
766
|
await assertSafeGeneratedPath(fileSystem, root, absolutePath, relativePath);
|