@phamvuhoang/otto-core 0.13.0 → 0.15.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/dist/cli-help.d.ts +10 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +18 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/context-budget.d.ts +78 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +114 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-report-cli.d.ts +36 -0
- package/dist/context-report-cli.d.ts.map +1 -0
- package/dist/context-report-cli.js +81 -0
- package/dist/context-report-cli.js.map +1 -0
- package/dist/context-report.d.ts +40 -0
- package/dist/context-report.d.ts.map +1 -0
- package/dist/context-report.js +88 -0
- package/dist/context-report.js.map +1 -0
- package/dist/eval-run.d.ts +16 -0
- package/dist/eval-run.d.ts.map +1 -1
- package/dist/eval-run.js +66 -1
- package/dist/eval-run.js.map +1 -1
- package/dist/eval.d.ts +2 -0
- package/dist/eval.d.ts.map +1 -1
- package/dist/eval.js +5 -0
- package/dist/eval.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/iteration-compaction.d.ts +73 -0
- package/dist/iteration-compaction.d.ts.map +1 -0
- package/dist/iteration-compaction.js +105 -0
- package/dist/iteration-compaction.js.map +1 -0
- package/dist/loop.d.ts +4 -0
- package/dist/loop.d.ts.map +1 -1
- package/dist/loop.js +25 -6
- package/dist/loop.js.map +1 -1
- package/dist/memory.d.ts +56 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +0 -0
- package/dist/memory.js.map +1 -1
- package/dist/read-dedup.d.ts +84 -0
- package/dist/read-dedup.d.ts.map +1 -0
- package/dist/read-dedup.js +96 -0
- package/dist/read-dedup.js.map +1 -0
- package/dist/risk.d.ts +7 -0
- package/dist/risk.d.ts.map +1 -1
- package/dist/risk.js +17 -0
- package/dist/risk.js.map +1 -1
- package/dist/run-bin.d.ts.map +1 -1
- package/dist/run-bin.js +17 -0
- package/dist/run-bin.js.map +1 -1
- package/dist/run-report.d.ts +25 -0
- package/dist/run-report.d.ts.map +1 -1
- package/dist/run-report.js.map +1 -1
- package/dist/runner.d.ts +4 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js.map +1 -1
- package/dist/runs-cli.d.ts +43 -0
- package/dist/runs-cli.d.ts.map +1 -0
- package/dist/runs-cli.js +96 -0
- package/dist/runs-cli.js.map +1 -0
- package/dist/skills-cli.d.ts +39 -0
- package/dist/skills-cli.d.ts.map +1 -0
- package/dist/skills-cli.js +137 -0
- package/dist/skills-cli.js.map +1 -0
- package/dist/skills.d.ts +195 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +320 -0
- package/dist/skills.js.map +1 -0
- package/dist/stage-exec.d.ts.map +1 -1
- package/dist/stage-exec.js +10 -3
- package/dist/stage-exec.js.map +1 -1
- package/dist/tokens.d.ts +21 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +20 -0
- package/dist/tokens.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { listRunIds, readManifest } from "./run-report.js";
|
|
3
|
+
import { findSkillCandidates, readSkills, selectSkills, skillsDir, skillStatus, } from "./skills.js";
|
|
4
|
+
const defaultDeps = {
|
|
5
|
+
env: process.env,
|
|
6
|
+
cwd: process.cwd(),
|
|
7
|
+
out: (m) => process.stdout.write(`${m}\n`),
|
|
8
|
+
err: (m) => process.stderr.write(`${m}\n`),
|
|
9
|
+
};
|
|
10
|
+
const USAGE = "Usage: otto-skills <list|audit|candidates>\n" +
|
|
11
|
+
" otto-skills why <changed-path>...";
|
|
12
|
+
/** One-line scope label: the globs, or "(repo-wide)" when empty. */
|
|
13
|
+
function scopeLabel(skill) {
|
|
14
|
+
return skill.scope.length ? skill.scope.join(", ") : "(repo-wide)";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Render the skill inventory with each skill's DERIVED validation status, trust,
|
|
18
|
+
* and capabilities/scope. Pure (clock injected). `now` drives freshness.
|
|
19
|
+
*/
|
|
20
|
+
export function formatSkillsReport(skills, now = new Date()) {
|
|
21
|
+
if (skills.length === 0) {
|
|
22
|
+
return "No skills yet. A skill is a .otto/skills/<name>/ package (skill.json + instructions.md).";
|
|
23
|
+
}
|
|
24
|
+
const lines = [];
|
|
25
|
+
for (const s of skills) {
|
|
26
|
+
const caps = s.capabilities.length ? s.capabilities.join(", ") : "(none)";
|
|
27
|
+
lines.push(`- ${s.name}@${s.version} [${skillStatus(s, now)}/${s.trust}] used ${s.useCount}x`);
|
|
28
|
+
lines.push(` capabilities: ${caps}`);
|
|
29
|
+
lines.push(` scope: ${scopeLabel(s)}`);
|
|
30
|
+
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Render a governance summary: how many skills are validated (auto-usable),
|
|
35
|
+
* unvalidated, or stale, and which are not yet usable. Pure (clock injected).
|
|
36
|
+
*/
|
|
37
|
+
export function formatSkillsAudit(skills, now = new Date()) {
|
|
38
|
+
const status = skills.map((s) => ({ s, st: skillStatus(s, now) }));
|
|
39
|
+
const count = (k) => status.filter((x) => x.st === k).length;
|
|
40
|
+
const lines = [];
|
|
41
|
+
lines.push(` total: ${skills.length}`);
|
|
42
|
+
lines.push(` validated: ${count("validated")}`);
|
|
43
|
+
lines.push(` unvalidated: ${count("unvalidated")}`);
|
|
44
|
+
lines.push(` stale: ${count("stale")}`);
|
|
45
|
+
const notUsable = status.filter((x) => x.st !== "validated");
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(`Not yet usable (${notUsable.length}):`);
|
|
48
|
+
if (notUsable.length === 0)
|
|
49
|
+
lines.push(" (none — all validated)");
|
|
50
|
+
for (const { s, st } of notUsable) {
|
|
51
|
+
lines.push(` - ${s.name} [${st}] (run its tests, then record a validating run)`);
|
|
52
|
+
}
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Render a retrieval explanation: the ranked {@link SkillMatch}es for a set of
|
|
57
|
+
* changed paths, each with whether it is eligible and WHY (the issue's
|
|
58
|
+
* "inspect why a skill was selected" metric). Pure.
|
|
59
|
+
*/
|
|
60
|
+
export function formatWhy(matches) {
|
|
61
|
+
if (matches.length === 0)
|
|
62
|
+
return "No skills to match.";
|
|
63
|
+
const lines = [];
|
|
64
|
+
for (const m of matches) {
|
|
65
|
+
lines.push(`- ${m.name} [${m.eligible ? "eligible" : "skip"}] score ${m.score}`);
|
|
66
|
+
for (const r of m.reasons)
|
|
67
|
+
lines.push(` · ${r}`);
|
|
68
|
+
}
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
/** Render the candidate skills suggested from repeated successful runs. Pure. */
|
|
72
|
+
export function formatCandidates(candidates) {
|
|
73
|
+
if (candidates.length === 0) {
|
|
74
|
+
return "No skill candidates: no successful workflow has repeated yet (need >= 2 runs of the same task).";
|
|
75
|
+
}
|
|
76
|
+
const lines = [];
|
|
77
|
+
for (const c of candidates) {
|
|
78
|
+
lines.push(`- ${c.suggestedName} (${c.count} successful runs)`);
|
|
79
|
+
lines.push(` signature: ${c.bin} / ${c.mode} / ${c.inputs || "(none)"}`);
|
|
80
|
+
lines.push(` runs: ${c.runIds.join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Drive the read-only `otto-skills` command over `.otto/skills/`. Subcommands:
|
|
86
|
+
* `list` (default) inventories skills + derived status; `audit` summarizes
|
|
87
|
+
* usable/unvalidated/stale; `why <path>...` shows which skills retrieval would
|
|
88
|
+
* select for those changed paths and why; `candidates` suggests skills from
|
|
89
|
+
* repeated successful runs. Read-only — it never executes a skill's tests or
|
|
90
|
+
* mutates a package. Resolves to the process exit code (mirrors `runMemory`).
|
|
91
|
+
*/
|
|
92
|
+
export async function runSkills(argv, deps = defaultDeps) {
|
|
93
|
+
const arg = argv[0];
|
|
94
|
+
if (arg === "-h" || arg === "--help") {
|
|
95
|
+
deps.out(USAGE);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
const known = ["list", "audit", "why", "candidates"];
|
|
99
|
+
if (arg !== undefined && !known.includes(arg)) {
|
|
100
|
+
deps.err(`Unknown subcommand '${arg}'.\n${USAGE}`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const workspaceDir = resolve(deps.env.OTTO_WORKSPACE ?? deps.cwd);
|
|
104
|
+
const skills = readSkills(workspaceDir);
|
|
105
|
+
if (arg === "why") {
|
|
106
|
+
const paths = argv.slice(1);
|
|
107
|
+
if (paths.length === 0) {
|
|
108
|
+
deps.err(`why needs at least one changed path.\n${USAGE}`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
deps.out(formatWhy(selectSkills(skills, { changedPaths: paths })));
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
if (arg === "candidates") {
|
|
115
|
+
const runs = listRunIds(workspaceDir)
|
|
116
|
+
.map((id) => readManifest(workspaceDir, id))
|
|
117
|
+
.filter((m) => m != null)
|
|
118
|
+
.map((m) => ({
|
|
119
|
+
runId: m.runId,
|
|
120
|
+
bin: m.bin,
|
|
121
|
+
mode: m.mode,
|
|
122
|
+
inputs: m.inputs,
|
|
123
|
+
exitReason: m.exitReason,
|
|
124
|
+
}));
|
|
125
|
+
deps.out(formatCandidates(findSkillCandidates(runs)));
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
if (arg === "audit") {
|
|
129
|
+
deps.out(`Skills audit (${skillsDir(workspaceDir)})`);
|
|
130
|
+
deps.out(formatSkillsAudit(skills));
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
deps.out(`Skills (${skillsDir(workspaceDir)})`);
|
|
134
|
+
deps.out(formatSkillsReport(skills));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=skills-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-cli.js","sourceRoot":"","sources":["../src/skills-cli.ts"],"names":[],"mappings":"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"}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { RunManifest } from "./run-report.js";
|
|
2
|
+
/**
|
|
3
|
+
* Skill extraction and reuse (issue #44 P5). A **skill** is a repo-local,
|
|
4
|
+
* versioned, validated procedure promoted from repeated successful trajectories
|
|
5
|
+
* — so future runs can retrieve a known workflow instead of re-planning it.
|
|
6
|
+
*
|
|
7
|
+
* Unlike a memory record (one JSON file), a skill is a **directory package**
|
|
8
|
+
* `.otto/skills/<name>/`: `skill.json` (this metadata) + `instructions.md` (the
|
|
9
|
+
* body), alongside optional scripts/tests. The `.otto/skills/` directory is the
|
|
10
|
+
* list — there is no central index, exactly like `.otto/runs/` and
|
|
11
|
+
* `.otto/memory/`. Pure fs + JSON, absent/malformed → safe defaults, never
|
|
12
|
+
* throws (mirrors `memory.ts`/`run-report.ts`).
|
|
13
|
+
*
|
|
14
|
+
* This module is a substrate: it is exported from `index.ts` and read by the
|
|
15
|
+
* read-only `otto-skills` bin, but **inert on the loop** — no run auto-selects or
|
|
16
|
+
* auto-applies a skill this slice, so a skill cannot regress a run. Retrieval,
|
|
17
|
+
* validation-gating, and candidate identification layer on top in later slices.
|
|
18
|
+
*/
|
|
19
|
+
/** Coarse provenance band, mirroring {@link MemoryTrust}. */
|
|
20
|
+
export type SkillTrust = "trusted" | "unverified" | "deprecated";
|
|
21
|
+
/**
|
|
22
|
+
* Validation provenance: the successful run that last proved the skill works.
|
|
23
|
+
* "Require validation before a skill is used automatically" (the issue) is
|
|
24
|
+
* enforced by retrieval filtering on the DERIVED {@link skillStatus}, which is
|
|
25
|
+
* computed from these fields — not by the bin executing the skill's tests.
|
|
26
|
+
*/
|
|
27
|
+
export type SkillValidation = {
|
|
28
|
+
/** Run id whose trajectory last validated this skill (absent = unvalidated). */
|
|
29
|
+
lastValidatedRun?: string;
|
|
30
|
+
/** When that validation happened (ISO). */
|
|
31
|
+
lastValidatedAt?: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* One skill package's metadata, stored as `.otto/skills/<name>/skill.json`. The
|
|
35
|
+
* `instructions` body lives in the sibling `instructions.md` (or inline here as a
|
|
36
|
+
* fallback) and is loaded by {@link readSkill}.
|
|
37
|
+
*/
|
|
38
|
+
export type Skill = {
|
|
39
|
+
/** Filesystem-safe package name; also the directory name. */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Free-text version (semver-ish); default "0.0.0". */
|
|
42
|
+
version: string;
|
|
43
|
+
/** Declared capability tags (e.g. "release-flow") — a retrieval key. */
|
|
44
|
+
capabilities: string[];
|
|
45
|
+
/** Guardrails — e.g. risk classes the skill must not be applied to. */
|
|
46
|
+
constraints: string[];
|
|
47
|
+
/** File/module globs the skill applies to; empty = repo-wide. A retrieval key. */
|
|
48
|
+
scope: string[];
|
|
49
|
+
/** The instruction body (from `instructions.md`, else the inline fallback). */
|
|
50
|
+
instructions: string;
|
|
51
|
+
/** Named helper commands the package ships (name → command string). */
|
|
52
|
+
scripts: Record<string, string>;
|
|
53
|
+
/** Validation command(s) that prove the skill still works. */
|
|
54
|
+
tests: string[];
|
|
55
|
+
validation: SkillValidation;
|
|
56
|
+
trust: SkillTrust;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
/** How many runs have consumed this skill. */
|
|
59
|
+
useCount: number;
|
|
60
|
+
/** Sliding revalidation window in days; past it a validated skill goes stale. */
|
|
61
|
+
revalidateAfterDays?: number;
|
|
62
|
+
};
|
|
63
|
+
/** Absolute path to the workspace's skills root (`.otto/skills`). */
|
|
64
|
+
export declare function skillsDir(workspaceDir: string): string;
|
|
65
|
+
/** Absolute path to one skill's package dir (`.otto/skills/<name>`). */
|
|
66
|
+
export declare function skillDir(workspaceDir: string, name: string): string;
|
|
67
|
+
/** Absolute path to a skill's metadata file (`.otto/skills/<name>/skill.json`). */
|
|
68
|
+
export declare function skillManifestPath(workspaceDir: string, name: string): string;
|
|
69
|
+
/** Absolute path to a skill's instructions body (`.../instructions.md`). */
|
|
70
|
+
export declare function skillInstructionsPath(workspaceDir: string, name: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Normalize free text into a filesystem-safe, git-branch-safe skill name:
|
|
73
|
+
* lowercase, every non-`[a-z0-9]` run collapsed to a single `-`, trimmed of
|
|
74
|
+
* leading/trailing `-`, capped at 48 chars. Mirrors `slugify`/`deriveTaskKey`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function toSkillName(raw: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Normalize an untrusted parsed `skill.json` value into a {@link Skill}, filling
|
|
79
|
+
* safe defaults for missing/invalid fields. Returns null when the input is not an
|
|
80
|
+
* object or lacks the required `name`, so a malformed package is skipped rather
|
|
81
|
+
* than crashing a read. `instructions` defaults to the inline value (if any);
|
|
82
|
+
* {@link readSkill} overrides it with the `instructions.md` body when present.
|
|
83
|
+
*/
|
|
84
|
+
export declare function parseSkill(raw: unknown): Skill | null;
|
|
85
|
+
/**
|
|
86
|
+
* List the skill names present under `.otto/skills/` (the package sub-dirs),
|
|
87
|
+
* sorted. Absent/unreadable dir → `[]` (never throws). The directory is the
|
|
88
|
+
* list; a name here is not a guarantee its `skill.json` parses (use
|
|
89
|
+
* {@link readSkill}).
|
|
90
|
+
*/
|
|
91
|
+
export declare function listSkillIds(workspaceDir: string): string[];
|
|
92
|
+
/**
|
|
93
|
+
* Read and normalize one skill package. Parses `skill.json`, then overrides
|
|
94
|
+
* `instructions` with the `instructions.md` body when that file exists. Absent or
|
|
95
|
+
* malformed `skill.json` → null (never throws).
|
|
96
|
+
*/
|
|
97
|
+
export declare function readSkill(workspaceDir: string, name: string): Skill | null;
|
|
98
|
+
/** Read every skill package under `.otto/skills/`, skipping malformed ones. */
|
|
99
|
+
export declare function readSkills(workspaceDir: string): Skill[];
|
|
100
|
+
/**
|
|
101
|
+
* Write a skill package: `skill.json` (metadata, with `instructions` omitted) +
|
|
102
|
+
* `instructions.md` (the body). Creates `.otto/skills/<name>/`. The split keeps
|
|
103
|
+
* the body as a readable, diff-friendly markdown file rather than an escaped JSON
|
|
104
|
+
* string.
|
|
105
|
+
*/
|
|
106
|
+
export declare function writeSkill(workspaceDir: string, skill: Skill): void;
|
|
107
|
+
/** True when a skill package directory with a `skill.json` exists. */
|
|
108
|
+
export declare function skillExists(workspaceDir: string, name: string): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Validation lifecycle (issue #44 slice 2). `unvalidated` = never proven by a
|
|
111
|
+
* run (so it must not be applied automatically); `validated` = a successful run
|
|
112
|
+
* proved it and it is within its freshness window; `stale` = it was validated but
|
|
113
|
+
* `revalidateAfterDays` have since elapsed and it needs re-proving.
|
|
114
|
+
*/
|
|
115
|
+
export type SkillStatus = "validated" | "unvalidated" | "stale";
|
|
116
|
+
/**
|
|
117
|
+
* Derive a skill's validation status from its recorded validation + freshness
|
|
118
|
+
* policy at `now`. A skill with no `validation.lastValidatedRun` is `unvalidated`;
|
|
119
|
+
* one validated but past its `revalidateAfterDays` window (measured from
|
|
120
|
+
* `lastValidatedAt`) is `stale`; otherwise `validated`. Unparseable timestamps
|
|
121
|
+
* are ignored rather than treated as expired (mirrors `memoryStatus`). Pure —
|
|
122
|
+
* the retrieval gate uses this so only `validated` skills are auto-eligible.
|
|
123
|
+
*/
|
|
124
|
+
export declare function skillStatus(skill: Skill, now?: Date): SkillStatus;
|
|
125
|
+
/**
|
|
126
|
+
* Return a copy of the skill marked validated by a successful `runId` at `now`.
|
|
127
|
+
* Pure — the input is not mutated; the caller writes it back to persist the
|
|
128
|
+
* validation (recording a validation is a run's job, never the read-only bin's).
|
|
129
|
+
*/
|
|
130
|
+
export declare function recordValidation(skill: Skill, runId: string, now?: Date): Skill;
|
|
131
|
+
/**
|
|
132
|
+
* Minimal path-glob match: `**` spans path separators, `*` spans a single
|
|
133
|
+
* segment, `?` one char. Used for a skill's scope globs against changed paths.
|
|
134
|
+
*/
|
|
135
|
+
export declare function globMatch(glob: string, path: string): boolean;
|
|
136
|
+
/** Context a retrieval is scored against (issue #44 slice 3). */
|
|
137
|
+
export type SkillMatchContext = {
|
|
138
|
+
/** The iteration's changed paths — drives scope + risk-class matching. */
|
|
139
|
+
changedPaths?: string[];
|
|
140
|
+
/** A required capability tag the caller is looking for. */
|
|
141
|
+
capability?: string;
|
|
142
|
+
/** Injectable clock for deterministic validation-freshness scoring. */
|
|
143
|
+
now?: Date;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* One skill's retrieval verdict: whether it is auto-eligible (validated AND not
|
|
147
|
+
* excluded by a constraint), a relevance `score`, and human `reasons[]` — so the
|
|
148
|
+
* operator can inspect *why* a skill was (or was not) selected (the issue's
|
|
149
|
+
* "users can inspect why a skill was selected" metric).
|
|
150
|
+
*/
|
|
151
|
+
export type SkillMatch = {
|
|
152
|
+
name: string;
|
|
153
|
+
eligible: boolean;
|
|
154
|
+
score: number;
|
|
155
|
+
reasons: string[];
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Rank skills for the current task. Retrieval is by **declared capability**,
|
|
159
|
+
* **touched files** (scope globs vs. changed paths), and **task risk** (a skill
|
|
160
|
+
* whose constraints forbid the change's risk class is excluded) — the three keys
|
|
161
|
+
* the issue names. Only `validated` skills are auto-`eligible` (validation before
|
|
162
|
+
* use); unvalidated/stale skills are still returned, flagged not-eligible with a
|
|
163
|
+
* reason, so the bin can show the full picture. Sorted eligible-first, then score
|
|
164
|
+
* desc, then name. Pure — no I/O, deterministic.
|
|
165
|
+
*/
|
|
166
|
+
export declare function selectSkills(skills: Skill[], ctx?: SkillMatchContext): SkillMatch[];
|
|
167
|
+
/** The run fields candidate identification needs (a subset of {@link RunManifest}). */
|
|
168
|
+
export type CandidateRun = Pick<RunManifest, "runId" | "bin" | "mode" | "inputs" | "exitReason">;
|
|
169
|
+
/**
|
|
170
|
+
* A repeated successful workflow worth promoting into a skill (issue #44 slice
|
|
171
|
+
* 4): the runs that share it, and a suggested package name. It is only a
|
|
172
|
+
* *suggestion* — extraction stays a maintainer action, never auto-promoted.
|
|
173
|
+
*/
|
|
174
|
+
export type SkillCandidate = {
|
|
175
|
+
/** Stable grouping signature `<bin>::<mode>::<inputs>`. */
|
|
176
|
+
signature: string;
|
|
177
|
+
bin: string;
|
|
178
|
+
mode: string;
|
|
179
|
+
inputs: string;
|
|
180
|
+
/** A filesystem-safe skill name suggestion derived from the signature. */
|
|
181
|
+
suggestedName: string;
|
|
182
|
+
/** Run ids of the successful runs sharing this signature, chronological. */
|
|
183
|
+
runIds: string[];
|
|
184
|
+
count: number;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Identify candidate skills from recorded runs: group **successful** runs
|
|
188
|
+
* (`exitReason` ∈ {complete, done}) by their `<bin>::<mode>::<inputs>` signature
|
|
189
|
+
* and surface every signature seen `minCount` (default 2) or more times. A
|
|
190
|
+
* conservative, deterministic heuristic — the same task succeeding repeatedly is
|
|
191
|
+
* the strongest signal that it is worth turning into a reusable procedure. Pure;
|
|
192
|
+
* no model call, no auto-promotion. Sorted by count desc, then signature.
|
|
193
|
+
*/
|
|
194
|
+
export declare function findSkillCandidates(runs: CandidateRun[], minCount?: number): SkillCandidate[];
|
|
195
|
+
//# sourceMappingURL=skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"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"}
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { classifyRisk } from "./risk.js";
|
|
4
|
+
const TRUSTS = new Set([
|
|
5
|
+
"trusted",
|
|
6
|
+
"unverified",
|
|
7
|
+
"deprecated",
|
|
8
|
+
]);
|
|
9
|
+
const SKILLS_REL = join(".otto", "skills");
|
|
10
|
+
const MANIFEST_FILE = "skill.json";
|
|
11
|
+
const INSTRUCTIONS_FILE = "instructions.md";
|
|
12
|
+
/** Absolute path to the workspace's skills root (`.otto/skills`). */
|
|
13
|
+
export function skillsDir(workspaceDir) {
|
|
14
|
+
return join(workspaceDir, SKILLS_REL);
|
|
15
|
+
}
|
|
16
|
+
/** Absolute path to one skill's package dir (`.otto/skills/<name>`). */
|
|
17
|
+
export function skillDir(workspaceDir, name) {
|
|
18
|
+
return join(skillsDir(workspaceDir), name);
|
|
19
|
+
}
|
|
20
|
+
/** Absolute path to a skill's metadata file (`.otto/skills/<name>/skill.json`). */
|
|
21
|
+
export function skillManifestPath(workspaceDir, name) {
|
|
22
|
+
return join(skillDir(workspaceDir, name), MANIFEST_FILE);
|
|
23
|
+
}
|
|
24
|
+
/** Absolute path to a skill's instructions body (`.../instructions.md`). */
|
|
25
|
+
export function skillInstructionsPath(workspaceDir, name) {
|
|
26
|
+
return join(skillDir(workspaceDir, name), INSTRUCTIONS_FILE);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalize free text into a filesystem-safe, git-branch-safe skill name:
|
|
30
|
+
* lowercase, every non-`[a-z0-9]` run collapsed to a single `-`, trimmed of
|
|
31
|
+
* leading/trailing `-`, capped at 48 chars. Mirrors `slugify`/`deriveTaskKey`.
|
|
32
|
+
*/
|
|
33
|
+
export function toSkillName(raw) {
|
|
34
|
+
return raw
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
37
|
+
.replace(/^-+|-+$/g, "")
|
|
38
|
+
.slice(0, 48);
|
|
39
|
+
}
|
|
40
|
+
function stringArray(raw) {
|
|
41
|
+
return Array.isArray(raw)
|
|
42
|
+
? raw.filter((s) => typeof s === "string")
|
|
43
|
+
: [];
|
|
44
|
+
}
|
|
45
|
+
function stringRecord(raw) {
|
|
46
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
47
|
+
return {};
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
50
|
+
if (typeof v === "string")
|
|
51
|
+
out[k] = v;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function parseValidation(raw) {
|
|
56
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
57
|
+
return {};
|
|
58
|
+
const o = raw;
|
|
59
|
+
const v = {};
|
|
60
|
+
if (typeof o.lastValidatedRun === "string") {
|
|
61
|
+
v.lastValidatedRun = o.lastValidatedRun;
|
|
62
|
+
}
|
|
63
|
+
if (typeof o.lastValidatedAt === "string") {
|
|
64
|
+
v.lastValidatedAt = o.lastValidatedAt;
|
|
65
|
+
}
|
|
66
|
+
return v;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Normalize an untrusted parsed `skill.json` value into a {@link Skill}, filling
|
|
70
|
+
* safe defaults for missing/invalid fields. Returns null when the input is not an
|
|
71
|
+
* object or lacks the required `name`, so a malformed package is skipped rather
|
|
72
|
+
* than crashing a read. `instructions` defaults to the inline value (if any);
|
|
73
|
+
* {@link readSkill} overrides it with the `instructions.md` body when present.
|
|
74
|
+
*/
|
|
75
|
+
export function parseSkill(raw) {
|
|
76
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
77
|
+
return null;
|
|
78
|
+
const o = raw;
|
|
79
|
+
if (typeof o.name !== "string" || o.name.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
return {
|
|
82
|
+
name: o.name,
|
|
83
|
+
version: typeof o.version === "string" ? o.version : "0.0.0",
|
|
84
|
+
capabilities: stringArray(o.capabilities),
|
|
85
|
+
constraints: stringArray(o.constraints),
|
|
86
|
+
scope: stringArray(o.scope),
|
|
87
|
+
instructions: typeof o.instructions === "string" ? o.instructions : "",
|
|
88
|
+
scripts: stringRecord(o.scripts),
|
|
89
|
+
tests: stringArray(o.tests),
|
|
90
|
+
validation: parseValidation(o.validation),
|
|
91
|
+
trust: typeof o.trust === "string" && TRUSTS.has(o.trust)
|
|
92
|
+
? o.trust
|
|
93
|
+
: "unverified",
|
|
94
|
+
createdAt: typeof o.createdAt === "string" ? o.createdAt : new Date(0).toISOString(),
|
|
95
|
+
useCount: typeof o.useCount === "number" && Number.isFinite(o.useCount)
|
|
96
|
+
? o.useCount
|
|
97
|
+
: 0,
|
|
98
|
+
...(typeof o.revalidateAfterDays === "number" &&
|
|
99
|
+
Number.isFinite(o.revalidateAfterDays)
|
|
100
|
+
? { revalidateAfterDays: o.revalidateAfterDays }
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* List the skill names present under `.otto/skills/` (the package sub-dirs),
|
|
106
|
+
* sorted. Absent/unreadable dir → `[]` (never throws). The directory is the
|
|
107
|
+
* list; a name here is not a guarantee its `skill.json` parses (use
|
|
108
|
+
* {@link readSkill}).
|
|
109
|
+
*/
|
|
110
|
+
export function listSkillIds(workspaceDir) {
|
|
111
|
+
try {
|
|
112
|
+
return readdirSync(skillsDir(workspaceDir), { withFileTypes: true })
|
|
113
|
+
.filter((e) => e.isDirectory())
|
|
114
|
+
.map((e) => e.name)
|
|
115
|
+
.sort();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Read and normalize one skill package. Parses `skill.json`, then overrides
|
|
123
|
+
* `instructions` with the `instructions.md` body when that file exists. Absent or
|
|
124
|
+
* malformed `skill.json` → null (never throws).
|
|
125
|
+
*/
|
|
126
|
+
export function readSkill(workspaceDir, name) {
|
|
127
|
+
let skill;
|
|
128
|
+
try {
|
|
129
|
+
skill = parseSkill(JSON.parse(readFileSync(skillManifestPath(workspaceDir, name), "utf8")));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (!skill)
|
|
135
|
+
return null;
|
|
136
|
+
try {
|
|
137
|
+
const body = readFileSync(skillInstructionsPath(workspaceDir, name), "utf8");
|
|
138
|
+
skill = { ...skill, instructions: body };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// No instructions.md sidecar — keep the inline (or empty) instructions.
|
|
142
|
+
}
|
|
143
|
+
return skill;
|
|
144
|
+
}
|
|
145
|
+
/** Read every skill package under `.otto/skills/`, skipping malformed ones. */
|
|
146
|
+
export function readSkills(workspaceDir) {
|
|
147
|
+
const skills = [];
|
|
148
|
+
for (const name of listSkillIds(workspaceDir)) {
|
|
149
|
+
const s = readSkill(workspaceDir, name);
|
|
150
|
+
if (s)
|
|
151
|
+
skills.push(s);
|
|
152
|
+
}
|
|
153
|
+
return skills;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Write a skill package: `skill.json` (metadata, with `instructions` omitted) +
|
|
157
|
+
* `instructions.md` (the body). Creates `.otto/skills/<name>/`. The split keeps
|
|
158
|
+
* the body as a readable, diff-friendly markdown file rather than an escaped JSON
|
|
159
|
+
* string.
|
|
160
|
+
*/
|
|
161
|
+
export function writeSkill(workspaceDir, skill) {
|
|
162
|
+
const dir = skillDir(workspaceDir, skill.name);
|
|
163
|
+
mkdirSync(dir, { recursive: true });
|
|
164
|
+
const { instructions, ...meta } = skill;
|
|
165
|
+
writeFileSync(join(dir, MANIFEST_FILE), JSON.stringify(meta, null, 2) + "\n");
|
|
166
|
+
writeFileSync(join(dir, INSTRUCTIONS_FILE), instructions);
|
|
167
|
+
}
|
|
168
|
+
/** True when a skill package directory with a `skill.json` exists. */
|
|
169
|
+
export function skillExists(workspaceDir, name) {
|
|
170
|
+
return existsSync(skillManifestPath(workspaceDir, name));
|
|
171
|
+
}
|
|
172
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
173
|
+
/** Parse an ISO timestamp to epoch ms; unparseable → null (never throws). */
|
|
174
|
+
function epoch(iso) {
|
|
175
|
+
if (typeof iso !== "string")
|
|
176
|
+
return null;
|
|
177
|
+
const t = Date.parse(iso);
|
|
178
|
+
return Number.isNaN(t) ? null : t;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Derive a skill's validation status from its recorded validation + freshness
|
|
182
|
+
* policy at `now`. A skill with no `validation.lastValidatedRun` is `unvalidated`;
|
|
183
|
+
* one validated but past its `revalidateAfterDays` window (measured from
|
|
184
|
+
* `lastValidatedAt`) is `stale`; otherwise `validated`. Unparseable timestamps
|
|
185
|
+
* are ignored rather than treated as expired (mirrors `memoryStatus`). Pure —
|
|
186
|
+
* the retrieval gate uses this so only `validated` skills are auto-eligible.
|
|
187
|
+
*/
|
|
188
|
+
export function skillStatus(skill, now = new Date()) {
|
|
189
|
+
if (!skill.validation.lastValidatedRun)
|
|
190
|
+
return "unvalidated";
|
|
191
|
+
if (skill.revalidateAfterDays !== undefined) {
|
|
192
|
+
const since = epoch(skill.validation.lastValidatedAt);
|
|
193
|
+
if (since !== null && now.getTime() - since > skill.revalidateAfterDays * DAY_MS) {
|
|
194
|
+
return "stale";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return "validated";
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Return a copy of the skill marked validated by a successful `runId` at `now`.
|
|
201
|
+
* Pure — the input is not mutated; the caller writes it back to persist the
|
|
202
|
+
* validation (recording a validation is a run's job, never the read-only bin's).
|
|
203
|
+
*/
|
|
204
|
+
export function recordValidation(skill, runId, now = new Date()) {
|
|
205
|
+
return {
|
|
206
|
+
...skill,
|
|
207
|
+
validation: { lastValidatedRun: runId, lastValidatedAt: now.toISOString() },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** Escape regex metacharacters outside the glob wildcards we handle. */
|
|
211
|
+
function escapeRe(s) {
|
|
212
|
+
return s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Minimal path-glob match: `**` spans path separators, `*` spans a single
|
|
216
|
+
* segment, `?` one char. Used for a skill's scope globs against changed paths.
|
|
217
|
+
*/
|
|
218
|
+
export function globMatch(glob, path) {
|
|
219
|
+
const re = escapeRe(glob)
|
|
220
|
+
.replace(/\*\*/g, "") // placeholder so the next step won't touch it
|
|
221
|
+
.replace(/\*/g, "[^/]*")
|
|
222
|
+
.replace(//g, ".*")
|
|
223
|
+
.replace(/\?/g, ".");
|
|
224
|
+
return new RegExp(`^${re}$`).test(path);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Rank skills for the current task. Retrieval is by **declared capability**,
|
|
228
|
+
* **touched files** (scope globs vs. changed paths), and **task risk** (a skill
|
|
229
|
+
* whose constraints forbid the change's risk class is excluded) — the three keys
|
|
230
|
+
* the issue names. Only `validated` skills are auto-`eligible` (validation before
|
|
231
|
+
* use); unvalidated/stale skills are still returned, flagged not-eligible with a
|
|
232
|
+
* reason, so the bin can show the full picture. Sorted eligible-first, then score
|
|
233
|
+
* desc, then name. Pure — no I/O, deterministic.
|
|
234
|
+
*/
|
|
235
|
+
export function selectSkills(skills, ctx = {}) {
|
|
236
|
+
const assessment = ctx.changedPaths && ctx.changedPaths.length > 0
|
|
237
|
+
? classifyRisk(ctx.changedPaths)
|
|
238
|
+
: null;
|
|
239
|
+
const matches = skills.map((skill) => {
|
|
240
|
+
const reasons = [];
|
|
241
|
+
let score = 0;
|
|
242
|
+
let eligible = true;
|
|
243
|
+
const status = skillStatus(skill, ctx.now);
|
|
244
|
+
if (status !== "validated") {
|
|
245
|
+
eligible = false;
|
|
246
|
+
reasons.push(`not eligible: ${status} (validation required before auto-use)`);
|
|
247
|
+
}
|
|
248
|
+
if (assessment) {
|
|
249
|
+
const forbidden = skill.constraints.some((c) => c.toLowerCase().includes(assessment.class));
|
|
250
|
+
if (forbidden) {
|
|
251
|
+
eligible = false;
|
|
252
|
+
reasons.push(`excluded by constraint for risk class "${assessment.class}"`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (ctx.capability) {
|
|
256
|
+
if (skill.capabilities.includes(ctx.capability)) {
|
|
257
|
+
score += 2;
|
|
258
|
+
reasons.push(`declares capability "${ctx.capability}"`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
reasons.push(`does not declare capability "${ctx.capability}"`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (ctx.changedPaths && ctx.changedPaths.length > 0) {
|
|
265
|
+
if (skill.scope.length === 0) {
|
|
266
|
+
score += 1;
|
|
267
|
+
reasons.push("repo-wide (no scope restriction)");
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const hit = ctx.changedPaths.filter((p) => skill.scope.some((g) => globMatch(g, p)));
|
|
271
|
+
if (hit.length > 0) {
|
|
272
|
+
score += 2;
|
|
273
|
+
reasons.push(`scope matches changed file(s): ${hit.join(", ")}`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
reasons.push("scope does not match the changed files");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { name: skill.name, eligible, score, reasons };
|
|
281
|
+
});
|
|
282
|
+
return matches.sort((a, b) => Number(b.eligible) - Number(a.eligible) ||
|
|
283
|
+
b.score - a.score ||
|
|
284
|
+
a.name.localeCompare(b.name));
|
|
285
|
+
}
|
|
286
|
+
const SUCCESS_REASONS = new Set(["complete", "done"]);
|
|
287
|
+
/**
|
|
288
|
+
* Identify candidate skills from recorded runs: group **successful** runs
|
|
289
|
+
* (`exitReason` ∈ {complete, done}) by their `<bin>::<mode>::<inputs>` signature
|
|
290
|
+
* and surface every signature seen `minCount` (default 2) or more times. A
|
|
291
|
+
* conservative, deterministic heuristic — the same task succeeding repeatedly is
|
|
292
|
+
* the strongest signal that it is worth turning into a reusable procedure. Pure;
|
|
293
|
+
* no model call, no auto-promotion. Sorted by count desc, then signature.
|
|
294
|
+
*/
|
|
295
|
+
export function findSkillCandidates(runs, minCount = 2) {
|
|
296
|
+
const groups = new Map();
|
|
297
|
+
for (const r of runs) {
|
|
298
|
+
if (r.exitReason == null || !SUCCESS_REASONS.has(r.exitReason))
|
|
299
|
+
continue;
|
|
300
|
+
const signature = `${r.bin}::${r.mode}::${r.inputs}`;
|
|
301
|
+
(groups.get(signature) ?? groups.set(signature, []).get(signature)).push(r);
|
|
302
|
+
}
|
|
303
|
+
const candidates = [];
|
|
304
|
+
for (const [signature, group] of groups) {
|
|
305
|
+
if (group.length < minCount)
|
|
306
|
+
continue;
|
|
307
|
+
const { bin, mode, inputs } = group[0];
|
|
308
|
+
candidates.push({
|
|
309
|
+
signature,
|
|
310
|
+
bin,
|
|
311
|
+
mode,
|
|
312
|
+
inputs,
|
|
313
|
+
suggestedName: toSkillName(`${mode}-${inputs}`) || toSkillName(mode),
|
|
314
|
+
runIds: group.map((r) => r.runId).sort(),
|
|
315
|
+
count: group.length,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return candidates.sort((a, b) => b.count - a.count || a.signature.localeCompare(b.signature));
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=skills.js.map
|