synorch 0.2.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +314 -28
  3. package/dist/application/doctor-service.d.ts +16 -0
  4. package/dist/application/doctor-service.d.ts.map +1 -1
  5. package/dist/application/doctor-service.js +230 -32
  6. package/dist/application/doctor-service.js.map +1 -1
  7. package/dist/application/generated-skill-doctor.d.ts +9 -0
  8. package/dist/application/generated-skill-doctor.d.ts.map +1 -0
  9. package/dist/application/generated-skill-doctor.js +433 -0
  10. package/dist/application/generated-skill-doctor.js.map +1 -0
  11. package/dist/application/project-discovery.d.ts +12 -0
  12. package/dist/application/project-discovery.d.ts.map +1 -1
  13. package/dist/application/project-discovery.js +65 -2
  14. package/dist/application/project-discovery.js.map +1 -1
  15. package/dist/application/safe-path.d.ts +19 -0
  16. package/dist/application/safe-path.d.ts.map +1 -0
  17. package/dist/application/safe-path.js +35 -0
  18. package/dist/application/safe-path.js.map +1 -0
  19. package/dist/application/size-ceiling.d.ts +15 -0
  20. package/dist/application/size-ceiling.d.ts.map +1 -0
  21. package/dist/application/size-ceiling.js +32 -0
  22. package/dist/application/size-ceiling.js.map +1 -0
  23. package/dist/application/structure-service.d.ts.map +1 -1
  24. package/dist/application/structure-service.js +10 -1
  25. package/dist/application/structure-service.js.map +1 -1
  26. package/dist/cli.js +10 -1
  27. package/dist/cli.js.map +1 -1
  28. package/dist/domain/canonical-contracts.d.ts +69 -0
  29. package/dist/domain/canonical-contracts.d.ts.map +1 -0
  30. package/dist/domain/canonical-contracts.js +92 -0
  31. package/dist/domain/canonical-contracts.js.map +1 -0
  32. package/dist/domain/generated-skill.d.ts +55 -0
  33. package/dist/domain/generated-skill.d.ts.map +1 -0
  34. package/dist/domain/generated-skill.js +90 -0
  35. package/dist/domain/generated-skill.js.map +1 -0
  36. package/dist/domain/generation.d.ts +15 -2
  37. package/dist/domain/generation.d.ts.map +1 -1
  38. package/dist/domain/observation-ledger.d.ts +118 -0
  39. package/dist/domain/observation-ledger.d.ts.map +1 -0
  40. package/dist/domain/observation-ledger.js +183 -0
  41. package/dist/domain/observation-ledger.js.map +1 -0
  42. package/dist/domain/product.d.ts +1 -1
  43. package/dist/domain/product.js +1 -1
  44. package/dist/domain/relative-path.d.ts +21 -0
  45. package/dist/domain/relative-path.d.ts.map +1 -0
  46. package/dist/domain/relative-path.js +43 -0
  47. package/dist/domain/relative-path.js.map +1 -0
  48. package/dist/domain/skill-packs.d.ts.map +1 -1
  49. package/dist/domain/skill-packs.js +1 -0
  50. package/dist/domain/skill-packs.js.map +1 -1
  51. package/dist/domain/zod-issues.d.ts +21 -0
  52. package/dist/domain/zod-issues.d.ts.map +1 -0
  53. package/dist/domain/zod-issues.js +27 -0
  54. package/dist/domain/zod-issues.js.map +1 -0
  55. package/dist/infrastructure/frontmatter.d.ts +25 -0
  56. package/dist/infrastructure/frontmatter.d.ts.map +1 -0
  57. package/dist/infrastructure/frontmatter.js +75 -0
  58. package/dist/infrastructure/frontmatter.js.map +1 -0
  59. package/dist/templates/agent-manifests.d.ts +19 -0
  60. package/dist/templates/agent-manifests.d.ts.map +1 -0
  61. package/dist/templates/agent-manifests.js +302 -0
  62. package/dist/templates/agent-manifests.js.map +1 -0
  63. package/dist/templates/base-skills.d.ts +23 -0
  64. package/dist/templates/base-skills.d.ts.map +1 -0
  65. package/dist/templates/base-skills.js +477 -0
  66. package/dist/templates/base-skills.js.map +1 -0
  67. package/dist/templates/skill-creator-skill.d.ts +23 -0
  68. package/dist/templates/skill-creator-skill.d.ts.map +1 -0
  69. package/dist/templates/skill-creator-skill.js +235 -0
  70. package/dist/templates/skill-creator-skill.js.map +1 -0
  71. package/dist/templates/skill-references.d.ts +13 -0
  72. package/dist/templates/skill-references.d.ts.map +1 -0
  73. package/dist/templates/skill-references.js +280 -0
  74. package/dist/templates/skill-references.js.map +1 -0
  75. package/dist/templates/structure-templates.d.ts.map +1 -1
  76. package/dist/templates/structure-templates.js +15 -139
  77. package/dist/templates/structure-templates.js.map +1 -1
  78. package/dist/templates/task-conductor-skill.d.ts +1 -1
  79. package/dist/templates/task-conductor-skill.d.ts.map +1 -1
  80. package/dist/templates/task-conductor-skill.js +50 -22
  81. package/dist/templates/task-conductor-skill.js.map +1 -1
  82. package/package.json +1 -1
@@ -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;AAG7B,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;CAC1C;AAED,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;YAsK5E,sBAAsB;YA4BtB,cAAc;YAwBd,SAAS;YAOT,cAAc;YAsBd,eAAe;YA0Df,2BAA2B;YAoC3B,sBAAsB;YAQtB,aAAa;CA+H5B"}
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 { projects, writtenFiles };
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
- function resolveWithinRoot(root, relativePath) {
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);