@nseng-ai/areg 0.1.1

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 (45) hide show
  1. package/package.json +29 -0
  2. package/src/cli.ts +109 -0
  3. package/src/context.ts +59 -0
  4. package/src/fake-gateways.ts +1009 -0
  5. package/src/gateways/command-constants.ts +1 -0
  6. package/src/gateways/errors.ts +5 -0
  7. package/src/gateways/fs-utils.ts +26 -0
  8. package/src/gateways/github-gateway.ts +111 -0
  9. package/src/gateways/host-gateway.ts +35 -0
  10. package/src/gateways/mutation-policy.ts +94 -0
  11. package/src/gateways/npx-skills-gateway.ts +53 -0
  12. package/src/gateways/project-fs.ts +320 -0
  13. package/src/gateways/project-gateway.ts +801 -0
  14. package/src/gateways/prompt-gateway.ts +21 -0
  15. package/src/gateways/skill-kind-classification.ts +39 -0
  16. package/src/gateways/skillx-workspace-gateway.ts +220 -0
  17. package/src/gateways.ts +361 -0
  18. package/src/index.ts +48 -0
  19. package/src/operations/check.ts +559 -0
  20. package/src/operations/doctor-skills-report.ts +109 -0
  21. package/src/operations/doctor-skills-severity.ts +9 -0
  22. package/src/operations/doctor-skills.ts +471 -0
  23. package/src/operations/file-state.ts +77 -0
  24. package/src/operations/frontmatter.ts +151 -0
  25. package/src/operations/init.ts +548 -0
  26. package/src/operations/lockfile.ts +107 -0
  27. package/src/operations/managed-markdown-block.ts +47 -0
  28. package/src/operations/pi-replacement.ts +33 -0
  29. package/src/operations/pi-settings.ts +60 -0
  30. package/src/operations/project-agents.ts +115 -0
  31. package/src/operations/project-inspection.ts +165 -0
  32. package/src/operations/project-mutations.ts +383 -0
  33. package/src/operations/project-resolution.ts +53 -0
  34. package/src/operations/skill-find.ts +295 -0
  35. package/src/operations/skill-kind-apply-plan.ts +531 -0
  36. package/src/operations/skill-kind-frontmatter.ts +55 -0
  37. package/src/operations/skill-kind-inference.ts +391 -0
  38. package/src/operations/skill-kind.ts +525 -0
  39. package/src/operations/skill-mirror-conventions.ts +126 -0
  40. package/src/operations/skillx.ts +305 -0
  41. package/src/operations/toml-section.ts +53 -0
  42. package/src/operations/update-skills.ts +325 -0
  43. package/src/real-gateways.ts +6 -0
  44. package/src/skill-lookup.ts +254 -0
  45. package/src/sort.ts +7 -0
@@ -0,0 +1,391 @@
1
+ import { err, type Result } from "@nseng-ai/foundation/result";
2
+
3
+ import type { AregSkillKindSkillInspection } from "../gateways.ts";
4
+ import { sortStrings } from "../sort.ts";
5
+ import { parseSkillFrontmatterBlock, type SkillFrontmatterData } from "./frontmatter.ts";
6
+ import {
7
+ formatReplacementLabel,
8
+ replacementAdvice,
9
+ verifyPiReplacement,
10
+ type PiReplacementVerification,
11
+ } from "./pi-replacement.ts";
12
+ import { parsePiSettings } from "./pi-settings.ts";
13
+ import type { AregSkillKindProjectInspection } from "./project-inspection.ts";
14
+
15
+ export const SKILL_INVOCATION_KINDS = [
16
+ "normal",
17
+ "invoke-only",
18
+ "command-backed",
19
+ "ambient-only",
20
+ "unlisted",
21
+ ] as const;
22
+ export const INFERRED_SKILL_INVOCATION_KINDS = [
23
+ ...SKILL_INVOCATION_KINDS,
24
+ "mixed",
25
+ "inconsistent",
26
+ ] as const;
27
+ export const MODEL_INVOCATION_STATUSES = ["enabled", "disabled", "mixed"] as const;
28
+ export const NATIVE_DIRECT_STATUSES = ["enabled", "partial", "hidden", "mixed"] as const;
29
+ export const PI_EXTENSION_STATUSES = ["n/a", "enabled", "excluded", "missing"] as const;
30
+ export const DISABLE_MODEL_INVOCATION_KEY = "disable-model-invocation";
31
+ export const USER_INVOCABLE_KEY = "user-invocable";
32
+ export const MANAGED_OPENAI_POLICY = "policy:\n allow_implicit_invocation: false\n";
33
+ const LEGACY_BARE_OPENAI_POLICY = "allow_implicit_invocation: false\n";
34
+
35
+ export type SkillInvocationKind = (typeof SKILL_INVOCATION_KINDS)[number];
36
+ export type InferredSkillInvocationKind = (typeof INFERRED_SKILL_INVOCATION_KINDS)[number];
37
+ export type ModelInvocationStatus = (typeof MODEL_INVOCATION_STATUSES)[number];
38
+ export type NativeDirectStatus = (typeof NATIVE_DIRECT_STATUSES)[number];
39
+ export type PiExtensionStatus = (typeof PI_EXTENSION_STATUSES)[number];
40
+
41
+ export interface SkillKindProperties {
42
+ shouldDisableModelInvocation: boolean;
43
+ hasCodexSidecar: boolean;
44
+ isPiExcluded: boolean;
45
+ }
46
+
47
+ export const KIND_PROPERTIES: Record<SkillInvocationKind, SkillKindProperties> = {
48
+ normal: {
49
+ shouldDisableModelInvocation: false,
50
+ hasCodexSidecar: false,
51
+ isPiExcluded: false,
52
+ },
53
+ "invoke-only": {
54
+ shouldDisableModelInvocation: true,
55
+ hasCodexSidecar: true,
56
+ isPiExcluded: false,
57
+ },
58
+ "command-backed": {
59
+ shouldDisableModelInvocation: true,
60
+ hasCodexSidecar: true,
61
+ isPiExcluded: true,
62
+ },
63
+ "ambient-only": {
64
+ shouldDisableModelInvocation: false,
65
+ hasCodexSidecar: false,
66
+ isPiExcluded: false,
67
+ },
68
+ unlisted: {
69
+ shouldDisableModelInvocation: true,
70
+ hasCodexSidecar: true,
71
+ isPiExcluded: true,
72
+ },
73
+ };
74
+
75
+ export interface SkillKindArtifactFacts {
76
+ isModelInvocationDisabled: boolean;
77
+ hasCodexSidecar: boolean;
78
+ hasUserInvocableKey: boolean;
79
+ isUserInvocableFalse: boolean;
80
+ isPiExcluded: boolean;
81
+ hasAgentsMirror: boolean;
82
+ hasClaudeMirror: boolean;
83
+ }
84
+
85
+ export interface SkillKindReplacementInfo {
86
+ verified: boolean;
87
+ surface?: string;
88
+ label: string;
89
+ evidence?: string;
90
+ advice?: string;
91
+ }
92
+
93
+ export interface SkillKindRecord {
94
+ skill: string;
95
+ kind: InferredSkillInvocationKind;
96
+ modelInvocation: ModelInvocationStatus;
97
+ nativeDirect: NativeDirectStatus;
98
+ piExtension: PiExtensionStatus;
99
+ artifacts: SkillKindArtifactFacts;
100
+ replacement: SkillKindReplacementInfo;
101
+ notes: readonly string[];
102
+ }
103
+
104
+ export type SkillKindProjectInspection = AregSkillKindProjectInspection;
105
+ export type FrontmatterInspection = SkillFrontmatterData;
106
+
107
+ export function inspectSkillFrontmatter(
108
+ text: string,
109
+ pathLabel: string,
110
+ ): Result<FrontmatterInspection> {
111
+ const parsed = parseSkillFrontmatterBlock(text);
112
+ if (!parsed.ok)
113
+ return {
114
+ ok: false,
115
+ error: { ...parsed.error, message: `${pathLabel} ${parsed.error.message}` },
116
+ };
117
+ return parsed;
118
+ }
119
+
120
+ export function inferSkillKindRecord(options: {
121
+ skillName: string;
122
+ frontmatter: FrontmatterInspection;
123
+ hasCodexSidecar: boolean;
124
+ isPiExcluded: boolean;
125
+ hasAgentsMirror: boolean;
126
+ hasClaudeMirror: boolean;
127
+ replacement: PiReplacementVerification;
128
+ }): SkillKindRecord {
129
+ const isModelInvocationDisabled = truthyFrontmatterValue(
130
+ options.frontmatter.fields[DISABLE_MODEL_INVOCATION_KEY],
131
+ );
132
+ const hasUserInvocableKey = options.frontmatter.keys.has(USER_INVOCABLE_KEY);
133
+ const isUserInvocableFalse = falsyFrontmatterValue(
134
+ options.frontmatter.fields[USER_INVOCABLE_KEY],
135
+ );
136
+ const artifacts: SkillKindArtifactFacts = {
137
+ isModelInvocationDisabled,
138
+ hasCodexSidecar: options.hasCodexSidecar,
139
+ hasUserInvocableKey,
140
+ isUserInvocableFalse,
141
+ isPiExcluded: options.isPiExcluded,
142
+ hasAgentsMirror: options.hasAgentsMirror,
143
+ hasClaudeMirror: options.hasClaudeMirror,
144
+ };
145
+ const kind = inferKind(artifacts, options.replacement);
146
+ const replacementEvidenceText = options.replacement.verified
147
+ ? replacementEvidence(options.replacement)
148
+ : undefined;
149
+ const replacementAdviceText = options.replacement.verified
150
+ ? undefined
151
+ : replacementAdvice(options.skillName, options.replacement.surface);
152
+ const replacement: SkillKindReplacementInfo = {
153
+ verified: options.replacement.verified,
154
+ ...(options.replacement.surface === undefined ? {} : { surface: options.replacement.surface }),
155
+ label: formatReplacementLabel(options.replacement),
156
+ ...(replacementEvidenceText === undefined ? {} : { evidence: replacementEvidenceText }),
157
+ ...(replacementAdviceText === undefined ? {} : { advice: replacementAdviceText }),
158
+ };
159
+ return {
160
+ skill: options.skillName,
161
+ kind,
162
+ modelInvocation: modelInvocationStatus(artifacts),
163
+ nativeDirect: nativeDirectStatus(kind, artifacts),
164
+ piExtension: piExtensionStatus(kind, artifacts, options.replacement),
165
+ artifacts,
166
+ replacement,
167
+ notes: buildNotes({
168
+ skillName: options.skillName,
169
+ kind,
170
+ artifacts,
171
+ userInvocableValue: options.frontmatter.fields[USER_INVOCABLE_KEY],
172
+ replacement: options.replacement,
173
+ }),
174
+ };
175
+ }
176
+
177
+ export function isManagedOpenaiPolicyContent(text: string): boolean {
178
+ return text === MANAGED_OPENAI_POLICY;
179
+ }
180
+
181
+ export function isLegacyBareOpenaiPolicyContent(text: string): boolean {
182
+ return text === LEGACY_BARE_OPENAI_POLICY;
183
+ }
184
+
185
+ export function buildSkillKindRecords(
186
+ inspection: SkillKindProjectInspection,
187
+ ): Result<readonly SkillKindRecord[]> {
188
+ const piSettings = parsePiSettings(inspection.piDir, inspection.piSettings);
189
+ if (!piSettings.ok) return piSettings;
190
+ const records: SkillKindRecord[] = [];
191
+ for (const skill of sortSkills(inspection.skills)) {
192
+ const readiness = validateInspectableSkill(skill);
193
+ if (!readiness.ok) return readiness;
194
+ if (skill.skillMd.type !== "file")
195
+ return err({
196
+ code: "skill_not_found",
197
+ message: `${skill.baseRelativePath}/SKILL.md does not exist`,
198
+ });
199
+ const frontmatter = inspectSkillFrontmatter(
200
+ skill.skillMd.text,
201
+ `${skill.baseRelativePath}/SKILL.md`,
202
+ );
203
+ if (!frontmatter.ok) return frontmatter;
204
+ const replacement = verifyPiReplacement(skill.name, inspection.replacement);
205
+ records.push(
206
+ inferSkillKindRecord({
207
+ skillName: skill.name,
208
+ frontmatter: frontmatter.value,
209
+ hasCodexSidecar: skill.openaiPolicy.type === "file",
210
+ isPiExcluded: piSettings.value.exclusions.includes(`-skills/${skill.name}`),
211
+ hasAgentsMirror: skill.agentsPath.type !== "missing",
212
+ hasClaudeMirror: skill.claudePath.type !== "missing",
213
+ replacement,
214
+ }),
215
+ );
216
+ }
217
+ return { ok: true, value: records };
218
+ }
219
+
220
+ export function validateInspectableSkill(skill: AregSkillKindSkillInspection): Result<undefined> {
221
+ if (skill.skillDir.type === "symlink")
222
+ return err({
223
+ code: "path_symlink",
224
+ message: `${skill.baseRelativePath} is a symlink; refusing to manage invocation metadata`,
225
+ });
226
+ if (skill.skillDir.type !== "directory")
227
+ return err({
228
+ code: "skill_not_found",
229
+ message: `Managed skill missing source: ${skill.baseRelativePath}/ does not exist`,
230
+ });
231
+ if (skill.skillMd.type === "symlink")
232
+ return err({
233
+ code: "path_symlink",
234
+ message: `${skill.baseRelativePath}/SKILL.md is a symlink; refusing to manage invocation metadata`,
235
+ });
236
+ if (skill.skillMd.type !== "file")
237
+ return err({
238
+ code: "skill_not_found",
239
+ message: `${skill.baseRelativePath}/SKILL.md does not exist`,
240
+ });
241
+ return { ok: true, value: undefined };
242
+ }
243
+
244
+ function inferKind(
245
+ artifacts: SkillKindArtifactFacts,
246
+ replacement: PiReplacementVerification,
247
+ ): InferredSkillInvocationKind {
248
+ if (
249
+ artifacts.isModelInvocationDisabled &&
250
+ artifacts.hasCodexSidecar &&
251
+ artifacts.isPiExcluded &&
252
+ replacement.verified &&
253
+ !artifacts.hasUserInvocableKey
254
+ )
255
+ return "command-backed";
256
+ if (
257
+ isUnlistedCandidate(artifacts, replacement.surface) &&
258
+ artifacts.isPiExcluded &&
259
+ !artifacts.hasAgentsMirror &&
260
+ !artifacts.hasClaudeMirror
261
+ )
262
+ return "unlisted";
263
+ if (
264
+ artifacts.isModelInvocationDisabled &&
265
+ artifacts.hasCodexSidecar &&
266
+ !artifacts.isPiExcluded &&
267
+ !artifacts.hasUserInvocableKey
268
+ )
269
+ return "invoke-only";
270
+ if (
271
+ artifacts.isUserInvocableFalse &&
272
+ !artifacts.isModelInvocationDisabled &&
273
+ !artifacts.hasCodexSidecar &&
274
+ !artifacts.isPiExcluded
275
+ )
276
+ return "ambient-only";
277
+ if (
278
+ !artifacts.isModelInvocationDisabled &&
279
+ !artifacts.hasCodexSidecar &&
280
+ !artifacts.hasUserInvocableKey &&
281
+ !artifacts.isPiExcluded
282
+ )
283
+ return "normal";
284
+ if (
285
+ artifacts.hasUserInvocableKey &&
286
+ (artifacts.isModelInvocationDisabled || artifacts.hasCodexSidecar || artifacts.isPiExcluded)
287
+ )
288
+ return "mixed";
289
+ return "inconsistent";
290
+ }
291
+
292
+ export function isUnlistedCandidate(
293
+ artifacts: SkillKindArtifactFacts,
294
+ replacementSurface: string | undefined,
295
+ ): boolean {
296
+ return (
297
+ artifacts.isModelInvocationDisabled &&
298
+ artifacts.hasCodexSidecar &&
299
+ !artifacts.hasUserInvocableKey &&
300
+ replacementSurface === undefined
301
+ );
302
+ }
303
+
304
+ function modelInvocationStatus(artifacts: SkillKindArtifactFacts): ModelInvocationStatus {
305
+ if (artifacts.isModelInvocationDisabled && artifacts.hasCodexSidecar) return "disabled";
306
+ if (artifacts.isModelInvocationDisabled || artifacts.hasCodexSidecar) return "mixed";
307
+ return "enabled";
308
+ }
309
+
310
+ function nativeDirectStatus(
311
+ kind: InferredSkillInvocationKind,
312
+ artifacts: SkillKindArtifactFacts,
313
+ ): NativeDirectStatus {
314
+ if (kind === "normal" || kind === "invoke-only") return "enabled";
315
+ if (kind === "unlisted") return "hidden";
316
+ if (kind === "command-backed" || kind === "ambient-only") return "partial";
317
+ if (artifacts.hasUserInvocableKey || artifacts.isPiExcluded) return "mixed";
318
+ return "enabled";
319
+ }
320
+
321
+ function piExtensionStatus(
322
+ kind: InferredSkillInvocationKind,
323
+ artifacts: SkillKindArtifactFacts,
324
+ replacement: PiReplacementVerification,
325
+ ): PiExtensionStatus {
326
+ if (!artifacts.isPiExcluded) return "n/a";
327
+ if (kind === "unlisted") return "excluded";
328
+ return replacement.verified ? "enabled" : "missing";
329
+ }
330
+
331
+ function buildNotes(options: {
332
+ skillName: string;
333
+ kind: InferredSkillInvocationKind;
334
+ artifacts: SkillKindArtifactFacts;
335
+ userInvocableValue: string | undefined;
336
+ replacement: PiReplacementVerification;
337
+ }): readonly string[] {
338
+ const notes: string[] = [];
339
+ if (options.artifacts.isModelInvocationDisabled && !options.artifacts.hasCodexSidecar)
340
+ notes.push("disable-model-invocation is present but agents/openai.yaml is missing.");
341
+ if (options.artifacts.hasCodexSidecar && !options.artifacts.isModelInvocationDisabled)
342
+ notes.push("agents/openai.yaml is present but disable-model-invocation is absent.");
343
+ if (options.artifacts.hasUserInvocableKey && !options.artifacts.isUserInvocableFalse)
344
+ notes.push(
345
+ `user-invocable is present with value ${JSON.stringify(options.userInvocableValue ?? "")}, not false.`,
346
+ );
347
+ if (
348
+ options.artifacts.isUserInvocableFalse &&
349
+ (options.artifacts.isModelInvocationDisabled ||
350
+ options.artifacts.hasCodexSidecar ||
351
+ options.artifacts.isPiExcluded)
352
+ ) {
353
+ notes.push("user-invocable:false is mixed with explicit-only or Pi-exclusion artifacts.");
354
+ }
355
+ if (
356
+ options.artifacts.isPiExcluded &&
357
+ !options.replacement.verified &&
358
+ options.kind !== "unlisted"
359
+ )
360
+ notes.push("Pi skill exclusion is present without a verified replacement command.");
361
+ if (options.kind === "unlisted")
362
+ notes.push(
363
+ `unlisted hides this skill from all harness typeaheads; canonical source remains skills/${options.skillName}/.`,
364
+ );
365
+ if (options.kind === "ambient-only")
366
+ notes.push(
367
+ "ambient-only disables Claude native direct invocation; Pi and Codex native direct invocation are not enforced.",
368
+ );
369
+ return notes;
370
+ }
371
+
372
+ function truthyFrontmatterValue(value: string | undefined): boolean {
373
+ return value?.trim().toLowerCase() === "true";
374
+ }
375
+
376
+ function falsyFrontmatterValue(value: string | undefined): boolean {
377
+ return value?.trim().toLowerCase() === "false";
378
+ }
379
+
380
+ function replacementEvidence(replacement: PiReplacementVerification): string | undefined {
381
+ return replacement.surface === undefined ? "replacement verified" : `/${replacement.surface}`;
382
+ }
383
+
384
+ function sortSkills(
385
+ skills: readonly AregSkillKindSkillInspection[],
386
+ ): readonly AregSkillKindSkillInspection[] {
387
+ const byName = new Map(skills.map((skill) => [skill.name, skill]));
388
+ return sortStrings([...byName.keys()])
389
+ .map((name) => byName.get(name))
390
+ .filter((skill): skill is AregSkillKindSkillInspection => skill !== undefined);
391
+ }