vigiles 2.0.0 → 2.2.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/README.md +182 -134
- package/dist/action-gate.d.ts +28 -0
- package/dist/action-gate.js +73 -0
- package/dist/cli.js +705 -230
- package/dist/community-skills.d.ts +22 -0
- package/dist/community-skills.js +86 -0
- package/dist/compile-generator.d.ts +48 -0
- package/dist/compile-generator.js +322 -0
- package/dist/compile.d.ts +4 -0
- package/dist/compile.js +239 -45
- package/dist/coverage.d.ts +56 -0
- package/dist/coverage.js +178 -0
- package/dist/doc-refs.d.ts +60 -0
- package/dist/doc-refs.js +246 -0
- package/dist/eval.d.ts +62 -0
- package/dist/eval.js +174 -0
- package/dist/evolve.js +60 -125
- package/dist/frontmatter.d.ts +75 -0
- package/dist/frontmatter.js +263 -0
- package/dist/generate-schema.d.ts +51 -0
- package/dist/generate-schema.js +125 -0
- package/dist/generate-types.js +39 -1
- package/dist/harness-test.d.ts +38 -0
- package/dist/harness-test.js +129 -0
- package/dist/hash.d.ts +8 -0
- package/dist/hash.js +16 -0
- package/dist/inline.d.ts +22 -4
- package/dist/inline.js +60 -13
- package/dist/integrity.d.ts +29 -0
- package/dist/integrity.js +44 -0
- package/dist/linters.d.ts +5 -3
- package/dist/linters.js +144 -4
- package/dist/mock-model.d.ts +31 -0
- package/dist/mock-model.js +189 -0
- package/dist/orphans.d.ts +52 -0
- package/dist/orphans.js +124 -0
- package/dist/proofs.js +22 -16
- package/dist/refs.d.ts +44 -0
- package/dist/refs.js +144 -0
- package/dist/session.d.ts +97 -0
- package/dist/session.js +306 -0
- package/dist/sidecar.d.ts +35 -0
- package/dist/sidecar.js +102 -0
- package/dist/skill-driver.d.ts +77 -0
- package/dist/skill-driver.js +76 -0
- package/dist/skill-runtime.d.ts +101 -0
- package/dist/skill-runtime.js +289 -0
- package/dist/skill-test.d.ts +47 -0
- package/dist/skill-test.js +77 -0
- package/dist/spec.d.ts +119 -13
- package/dist/spec.js +51 -4
- package/dist/symbols.d.ts +30 -0
- package/dist/symbols.js +142 -0
- package/dist/test-utils.d.ts +8 -0
- package/dist/test-utils.js +41 -0
- package/dist/types.d.ts +34 -8
- package/dist/types.js +19 -0
- package/dist/validate.js +5 -3
- package/package.json +26 -5
- package/.claude/settings.json +0 -46
- package/.claude/settings.local.json +0 -8
- package/.github/workflows/ci.yml +0 -81
- package/.prettierignore +0 -1
- package/.vigiles/generated.d.ts +0 -205
- package/CLAUDE.md +0 -95
- package/CLAUDE.md.spec.ts +0 -142
- package/CONTRIBUTING.md +0 -121
- package/dist/action.d.ts.map +0 -1
- package/dist/action.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/cli.test.d.ts +0 -2
- package/dist/cli.test.d.ts.map +0 -1
- package/dist/cli.test.js +0 -650
- package/dist/cli.test.js.map +0 -1
- package/dist/compile.d.ts.map +0 -1
- package/dist/compile.js.map +0 -1
- package/dist/evolve.d.ts.map +0 -1
- package/dist/evolve.js.map +0 -1
- package/dist/freshness.d.ts +0 -67
- package/dist/freshness.d.ts.map +0 -1
- package/dist/freshness.js +0 -244
- package/dist/freshness.js.map +0 -1
- package/dist/freshness.test.d.ts +0 -2
- package/dist/freshness.test.d.ts.map +0 -1
- package/dist/freshness.test.js +0 -356
- package/dist/freshness.test.js.map +0 -1
- package/dist/generate-types.d.ts.map +0 -1
- package/dist/generate-types.js.map +0 -1
- package/dist/inline.d.ts.map +0 -1
- package/dist/inline.js.map +0 -1
- package/dist/inline.test.d.ts +0 -5
- package/dist/inline.test.d.ts.map +0 -1
- package/dist/inline.test.js +0 -152
- package/dist/inline.test.js.map +0 -1
- package/dist/linters.d.ts.map +0 -1
- package/dist/linters.js.map +0 -1
- package/dist/proofs.d.ts.map +0 -1
- package/dist/proofs.js.map +0 -1
- package/dist/proofs.test.d.ts +0 -9
- package/dist/proofs.test.d.ts.map +0 -1
- package/dist/proofs.test.js +0 -952
- package/dist/proofs.test.js.map +0 -1
- package/dist/spec.d.ts.map +0 -1
- package/dist/spec.js.map +0 -1
- package/dist/spec.test.d.ts +0 -2
- package/dist/spec.test.d.ts.map +0 -1
- package/dist/spec.test.js +0 -1222
- package/dist/spec.test.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js.map +0 -1
- package/dist/validate.test.d.ts +0 -2
- package/dist/validate.test.d.ts.map +0 -1
- package/dist/validate.test.js +0 -531
- package/dist/validate.test.js.map +0 -1
- package/docs/agent-setup.md +0 -85
- package/docs/agent-workflows.md +0 -103
- package/docs/comparison.md +0 -71
- package/docs/freshness.md +0 -124
- package/docs/inline-mode.md +0 -119
- package/docs/linter-support.md +0 -166
- package/docs/spec-format.md +0 -194
- package/eslint.config.mjs +0 -79
- package/examples/CLAUDE.md +0 -54
- package/examples/CLAUDE.md.spec.ts +0 -65
- package/examples/SKILL.md +0 -50
- package/examples/SKILL.md.spec.ts +0 -57
- package/fixtures/example-project/CLAUDE.md +0 -11
- package/fixtures/example-project/package.json +0 -9
- package/fixtures/example-project/src/index.ts +0 -3
- package/fixtures/example-project/src/utils.test.ts +0 -2
- package/fixtures/example-project/src/utils.ts +0 -3
- package/logo.png +0 -0
- package/research/adoption-strategy.md +0 -111
- package/research/agent-integration.md +0 -145
- package/research/ai-code-quality.md +0 -197
- package/research/code-search-for-agents.md +0 -313
- package/research/competitive-landscape.md +0 -163
- package/research/doc-freshness.md +0 -516
- package/research/executable-specs.md +0 -368
- package/research/feature-ideas.md +0 -464
- package/research/formal-proofs-for-agents.md +0 -338
- package/research/fp-for-agent-harness.md +0 -150
- package/research/fp-for-deterministic-ai.md +0 -131
- package/research/self-evolving-specs.md +0 -298
- package/schemas/claude-md-strict.yml +0 -18
- package/schemas/claude-md.yml +0 -6
- package/schemas/skill-strict.yml +0 -12
- package/schemas/skill.yml +0 -5
- package/skills/audit-feedback-loop/SKILL.md +0 -76
- package/skills/edit-spec/SKILL.md +0 -131
- package/skills/enforce-rules-format/SKILL.md +0 -71
- package/skills/generate-logo/SKILL.md +0 -102
- package/skills/generate-rule/SKILL.md +0 -90
- package/skills/linter-docs/clippy.md +0 -241
- package/skills/linter-docs/eslint.md +0 -384
- package/skills/linter-docs/pylint.md +0 -288
- package/skills/linter-docs/rubocop.md +0 -277
- package/skills/linter-docs/ruff.md +0 -187
- package/skills/linter-docs/stylelint.md +0 -247
- package/skills/migrate-to-spec/SKILL.md +0 -124
- package/skills/pr-to-lint-rule/SKILL.md +0 -97
- package/skills/strengthen/SKILL.md +0 -168
- package/src/action.ts +0 -214
- package/src/cli.test.ts +0 -914
- package/src/cli.ts +0 -1631
- package/src/compile.ts +0 -691
- package/src/evolve.ts +0 -781
- package/src/freshness.test.ts +0 -449
- package/src/freshness.ts +0 -299
- package/src/generate-types.ts +0 -448
- package/src/inline.test.ts +0 -206
- package/src/inline.ts +0 -164
- package/src/linters.ts +0 -739
- package/src/proofs.test.ts +0 -1314
- package/src/proofs.ts +0 -849
- package/src/spec.test.ts +0 -1471
- package/src/spec.ts +0 -427
- package/src/types.ts +0 -117
- package/src/validate.test.ts +0 -701
- package/src/validate.ts +0 -381
- package/tsconfig.json +0 -23
package/dist/sidecar.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-spec sidecar manifests stored at `.vigiles/<target>.inputs.json`.
|
|
4
|
+
*
|
|
5
|
+
* Used by the post-session audit to know which targets exist and which
|
|
6
|
+
* spec source / inputs each one tracks. The compile pipeline writes
|
|
7
|
+
* these whenever a spec is built; readers (currently only session.ts)
|
|
8
|
+
* consume them at audit time.
|
|
9
|
+
*
|
|
10
|
+
* This module is the ONLY place sidecars live now — the freshness rule
|
|
11
|
+
* doesn't depend on them anymore.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.sidecarPath = sidecarPath;
|
|
15
|
+
exports.writeSidecarManifest = writeSidecarManifest;
|
|
16
|
+
exports.readSidecarManifest = readSidecarManifest;
|
|
17
|
+
exports.computePerFileHashes = computePerFileHashes;
|
|
18
|
+
exports.iterateSidecars = iterateSidecars;
|
|
19
|
+
const node_fs_1 = require("node:fs");
|
|
20
|
+
const node_path_1 = require("node:path");
|
|
21
|
+
const hash_js_1 = require("./hash.js");
|
|
22
|
+
const SIDECAR_ROOT = ".vigiles";
|
|
23
|
+
const MANIFEST_SUFFIX = ".inputs.json";
|
|
24
|
+
function sidecarPath(basePath, target) {
|
|
25
|
+
return (0, node_path_1.resolve)(basePath, SIDECAR_ROOT, `${target}${MANIFEST_SUFFIX}`);
|
|
26
|
+
}
|
|
27
|
+
function writeSidecarManifest(basePath, manifest) {
|
|
28
|
+
// Targets can be nested (e.g. ".github/copilot-instructions.md"), so
|
|
29
|
+
// ensure every intermediate directory under .vigiles/ exists before
|
|
30
|
+
// writing — otherwise writeFileSync throws ENOENT.
|
|
31
|
+
const filePath = sidecarPath(basePath, manifest.target);
|
|
32
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(filePath), { recursive: true });
|
|
33
|
+
(0, node_fs_1.writeFileSync)(filePath, JSON.stringify(manifest, null, 2) + "\n");
|
|
34
|
+
}
|
|
35
|
+
function readSidecarManifest(basePath, target) {
|
|
36
|
+
const filePath = sidecarPath(basePath, target);
|
|
37
|
+
if (!(0, node_fs_1.existsSync)(filePath))
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf-8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function computePerFileHashes(inputFiles, basePath) {
|
|
47
|
+
const result = {};
|
|
48
|
+
for (const f of inputFiles) {
|
|
49
|
+
const fullPath = (0, node_path_1.resolve)(basePath, f);
|
|
50
|
+
result[f] = (0, node_fs_1.existsSync)(fullPath)
|
|
51
|
+
? (0, hash_js_1.sha256short)((0, node_fs_1.readFileSync)(fullPath))
|
|
52
|
+
: "MISSING";
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Iterate all sidecar manifests under `.vigiles/`, including nested
|
|
58
|
+
* directories. Handles directory-not-found and read errors gracefully.
|
|
59
|
+
*
|
|
60
|
+
* Recurses so that nested targets (e.g. `.github/copilot-instructions.md`,
|
|
61
|
+
* which lives at `.vigiles/.github/copilot-instructions.md.inputs.json`)
|
|
62
|
+
* are not silently skipped.
|
|
63
|
+
*/
|
|
64
|
+
function iterateSidecars(basePath, fn) {
|
|
65
|
+
const root = (0, node_path_1.resolve)(basePath, SIDECAR_ROOT);
|
|
66
|
+
if (!(0, node_fs_1.existsSync)(root))
|
|
67
|
+
return;
|
|
68
|
+
walk(root, root, basePath, fn);
|
|
69
|
+
}
|
|
70
|
+
function walk(dir, root, basePath, fn) {
|
|
71
|
+
let entries;
|
|
72
|
+
try {
|
|
73
|
+
entries = (0, node_fs_1.readdirSync)(dir);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const fullPath = (0, node_path_1.resolve)(dir, entry);
|
|
80
|
+
let isDir;
|
|
81
|
+
try {
|
|
82
|
+
isDir = (0, node_fs_1.statSync)(fullPath).isDirectory();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (isDir) {
|
|
88
|
+
walk(fullPath, root, basePath, fn);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!entry.endsWith(MANIFEST_SUFFIX))
|
|
92
|
+
continue;
|
|
93
|
+
// Reconstruct the target name from the path relative to .vigiles/
|
|
94
|
+
const rel = (0, node_path_1.relative)(root, fullPath);
|
|
95
|
+
const target = rel.slice(0, -MANIFEST_SUFFIX.length);
|
|
96
|
+
const manifest = readSidecarManifest(basePath, target);
|
|
97
|
+
if (!manifest)
|
|
98
|
+
continue;
|
|
99
|
+
fn(target, manifest);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=sidecar.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles — Skill driver: the generator (durable-imperative) form of a skill.
|
|
3
|
+
*
|
|
4
|
+
* The declarative `skill({ steps })` form compiles to a *static* SKILL.md — it
|
|
5
|
+
* can't express branching/looping, because that control flow depends on runtime
|
|
6
|
+
* values. The generator form does: a skill is a generator that `yield`s
|
|
7
|
+
* effects, and the harness drives it with `.next()`. Branches are real `if`,
|
|
8
|
+
* loops are real `for`/`while`; the harness owns the loop and runs the
|
|
9
|
+
* deterministic gates between yields, short-circuiting on failure (Railway).
|
|
10
|
+
*
|
|
11
|
+
* The model is an injected seam: `runStep(prose)` is what executes a prose step
|
|
12
|
+
* (in production it calls the LLM; in tests it's a scripted mock and the answer
|
|
13
|
+
* is fed back into the generator). Gates are deterministic and reuse the gate
|
|
14
|
+
* runtime. This module is the mechanics — fully testable without a live model.
|
|
15
|
+
*/
|
|
16
|
+
import type { Gate } from "./spec.js";
|
|
17
|
+
import { type GateOutcome } from "./skill-runtime.js";
|
|
18
|
+
export type SkillEffect = {
|
|
19
|
+
readonly kind: "act";
|
|
20
|
+
readonly prose: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: "gate";
|
|
23
|
+
readonly gate: Gate;
|
|
24
|
+
readonly retry: number;
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: "result";
|
|
27
|
+
readonly gate: Gate;
|
|
28
|
+
};
|
|
29
|
+
/** A prose step for the model to perform. Its answer is yielded back in. */
|
|
30
|
+
export declare function act(prose: string): SkillEffect;
|
|
31
|
+
/** A deterministic checkpoint gate the harness runs after the prior step(s). */
|
|
32
|
+
export declare function checkpoint(gate: Gate, retry?: number): SkillEffect;
|
|
33
|
+
/** The terminal result gate — the skill is done only when it passes. */
|
|
34
|
+
export declare function finish(gate: Gate): SkillEffect;
|
|
35
|
+
/**
|
|
36
|
+
* A skill program: a generator that yields effects and receives, for each
|
|
37
|
+
* `act`, the model's answer back in (`const x = yield act(...)`).
|
|
38
|
+
*/
|
|
39
|
+
export type SkillProgram = () => Generator<SkillEffect, void, string>;
|
|
40
|
+
/** Frontmatter metadata for a generator skill. */
|
|
41
|
+
export interface GeneratorSkillMeta {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly description: string;
|
|
44
|
+
readonly disableModelInvocation?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A generator skill = metadata + a generator program. Authored as
|
|
48
|
+
* `export default genSkill({ name, description }, function* () { … })`.
|
|
49
|
+
* The CLI compiles it to SKILL.md by parsing the source (it can't execute a
|
|
50
|
+
* generator to markdown); `runSkill`/`driveSkill` execute `program` directly.
|
|
51
|
+
*/
|
|
52
|
+
export interface GeneratorSkill extends GeneratorSkillMeta {
|
|
53
|
+
readonly _specType: "skill-generator";
|
|
54
|
+
readonly program: SkillProgram;
|
|
55
|
+
}
|
|
56
|
+
/** Define a generator skill (metadata + program). */
|
|
57
|
+
export declare function genSkill(meta: GeneratorSkillMeta, program: SkillProgram): GeneratorSkill;
|
|
58
|
+
export interface DriveStep {
|
|
59
|
+
readonly effect: SkillEffect;
|
|
60
|
+
/** The model's answer, for an `act` effect. */
|
|
61
|
+
readonly answer?: string;
|
|
62
|
+
/** The gate outcome, for a `gate`/`result` effect. */
|
|
63
|
+
readonly outcome?: GateOutcome;
|
|
64
|
+
}
|
|
65
|
+
export interface DriveReport {
|
|
66
|
+
readonly ok: boolean;
|
|
67
|
+
/** Index (in the trace) of the gate that blocked, or null if all passed. */
|
|
68
|
+
readonly blockedAt: number | null;
|
|
69
|
+
readonly trace: readonly DriveStep[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Drive a skill program to completion (or to the first failed gate). `runStep`
|
|
73
|
+
* is the model seam: it executes a prose step and returns the model's answer,
|
|
74
|
+
* which is fed back into the generator so branches can switch on it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function driveSkill(program: SkillProgram, cwd: string, runStep: (prose: string) => string): DriveReport;
|
|
77
|
+
//# sourceMappingURL=skill-driver.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles — Skill driver: the generator (durable-imperative) form of a skill.
|
|
4
|
+
*
|
|
5
|
+
* The declarative `skill({ steps })` form compiles to a *static* SKILL.md — it
|
|
6
|
+
* can't express branching/looping, because that control flow depends on runtime
|
|
7
|
+
* values. The generator form does: a skill is a generator that `yield`s
|
|
8
|
+
* effects, and the harness drives it with `.next()`. Branches are real `if`,
|
|
9
|
+
* loops are real `for`/`while`; the harness owns the loop and runs the
|
|
10
|
+
* deterministic gates between yields, short-circuiting on failure (Railway).
|
|
11
|
+
*
|
|
12
|
+
* The model is an injected seam: `runStep(prose)` is what executes a prose step
|
|
13
|
+
* (in production it calls the LLM; in tests it's a scripted mock and the answer
|
|
14
|
+
* is fed back into the generator). Gates are deterministic and reuse the gate
|
|
15
|
+
* runtime. This module is the mechanics — fully testable without a live model.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.act = act;
|
|
19
|
+
exports.checkpoint = checkpoint;
|
|
20
|
+
exports.finish = finish;
|
|
21
|
+
exports.genSkill = genSkill;
|
|
22
|
+
exports.driveSkill = driveSkill;
|
|
23
|
+
const skill_runtime_js_1 = require("./skill-runtime.js");
|
|
24
|
+
/** A prose step for the model to perform. Its answer is yielded back in. */
|
|
25
|
+
function act(prose) {
|
|
26
|
+
return { kind: "act", prose };
|
|
27
|
+
}
|
|
28
|
+
/** A deterministic checkpoint gate the harness runs after the prior step(s). */
|
|
29
|
+
function checkpoint(gate, retry = 1) {
|
|
30
|
+
return { kind: "gate", gate, retry };
|
|
31
|
+
}
|
|
32
|
+
/** The terminal result gate — the skill is done only when it passes. */
|
|
33
|
+
function finish(gate) {
|
|
34
|
+
return { kind: "result", gate };
|
|
35
|
+
}
|
|
36
|
+
/** Define a generator skill (metadata + program). */
|
|
37
|
+
function genSkill(meta, program) {
|
|
38
|
+
return { _specType: "skill-generator", ...meta, program };
|
|
39
|
+
}
|
|
40
|
+
/** Convert a spec-level Gate to the runtime gate the executor understands. */
|
|
41
|
+
function toRuntimeGate(gate, retry) {
|
|
42
|
+
if (gate._ref === "cmd")
|
|
43
|
+
return { kind: "cmd", command: gate.command, retry };
|
|
44
|
+
if (gate._ref === "role")
|
|
45
|
+
return { kind: "role", role: gate.role, retry };
|
|
46
|
+
return { kind: "file", path: gate.path, retry };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Drive a skill program to completion (or to the first failed gate). `runStep`
|
|
50
|
+
* is the model seam: it executes a prose step and returns the model's answer,
|
|
51
|
+
* which is fed back into the generator so branches can switch on it.
|
|
52
|
+
*/
|
|
53
|
+
function driveSkill(program, cwd, runStep) {
|
|
54
|
+
const gen = program();
|
|
55
|
+
const trace = [];
|
|
56
|
+
let input = "";
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { value: effect, done } = gen.next(input);
|
|
59
|
+
if (done)
|
|
60
|
+
break;
|
|
61
|
+
if (effect.kind === "act") {
|
|
62
|
+
const answer = runStep(effect.prose);
|
|
63
|
+
trace.push({ effect, answer });
|
|
64
|
+
input = answer;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const retry = effect.kind === "gate" ? effect.retry : 1;
|
|
68
|
+
const outcome = (0, skill_runtime_js_1.runGate)(toRuntimeGate(effect.gate, retry), cwd);
|
|
69
|
+
trace.push({ effect, outcome });
|
|
70
|
+
if (!outcome.ok)
|
|
71
|
+
return { ok: false, blockedAt: trace.length - 1, trace };
|
|
72
|
+
input = "";
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, blockedAt: null, trace };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=skill-driver.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles — Skill runtime: the deterministic gate ladder.
|
|
3
|
+
*
|
|
4
|
+
* Parses the `vigiles:gate` / `vigiles:result` markers a compiled SKILL.md
|
|
5
|
+
* carries (emitted by compileSkill) and *executes* them: run each step's gate
|
|
6
|
+
* in document order, short-circuit on the first failure (Railway error track),
|
|
7
|
+
* then run the terminal result gate. This is the deterministic spine of the
|
|
8
|
+
* skill driver — the part that turns the markers from documentation into
|
|
9
|
+
* enforcement.
|
|
10
|
+
*
|
|
11
|
+
* Scope (v0): this runs the GATES only. It does not drive the LLM through the
|
|
12
|
+
* prose steps one at a time — that needs a live harness and is a later phase.
|
|
13
|
+
* `retry:N` is parsed and surfaced but executed once here: re-running a
|
|
14
|
+
* deterministic gate without a model in the loop to fix the step yields the
|
|
15
|
+
* same result. Retry is meaningful only once the model re-does the step
|
|
16
|
+
* between attempts.
|
|
17
|
+
*
|
|
18
|
+
* Safety: this executes the gate commands the skill author declared (e.g.
|
|
19
|
+
* `npm test`, `validate.py`) via an explicit, user-invoked command
|
|
20
|
+
* (`vigiles run-skill`). It is not a silent hook and runs nothing the spec
|
|
21
|
+
* didn't declare as a gate.
|
|
22
|
+
*/
|
|
23
|
+
export type RuntimeGate = {
|
|
24
|
+
readonly kind: "cmd";
|
|
25
|
+
readonly command: string;
|
|
26
|
+
readonly retry: number;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "file";
|
|
29
|
+
readonly path: string;
|
|
30
|
+
readonly retry: number;
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: "role";
|
|
33
|
+
readonly role: string;
|
|
34
|
+
readonly retry: number;
|
|
35
|
+
};
|
|
36
|
+
export interface SkillGates {
|
|
37
|
+
/** Per-step gates, in document order. */
|
|
38
|
+
readonly steps: readonly {
|
|
39
|
+
readonly step: number;
|
|
40
|
+
readonly gate: RuntimeGate;
|
|
41
|
+
}[];
|
|
42
|
+
/** Terminal postcondition gate, if any. */
|
|
43
|
+
readonly result?: RuntimeGate;
|
|
44
|
+
}
|
|
45
|
+
/** Extract the step gates and result gate from a compiled SKILL.md. */
|
|
46
|
+
export declare function parseSkillGates(markdown: string): SkillGates;
|
|
47
|
+
export interface GateOutcome {
|
|
48
|
+
readonly ok: boolean;
|
|
49
|
+
readonly output: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a project role (test/build/lint) to the host project's real command,
|
|
53
|
+
* detected from its ecosystem. Returns null when no command can be found —
|
|
54
|
+
* which the caller surfaces as a failed gate rather than a silent pass.
|
|
55
|
+
*/
|
|
56
|
+
export declare function detectProjectCommand(role: string, cwd: string): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Run one gate against `cwd`: a command (exit 0 = pass), a file existence, or
|
|
59
|
+
* a project role resolved to the host project's command.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runGate(gate: RuntimeGate, cwd: string): GateOutcome;
|
|
62
|
+
/** Human-readable label for a gate (for reports and hook messages). */
|
|
63
|
+
export declare function gateLabel(gate: RuntimeGate): string;
|
|
64
|
+
export interface GateRunResult {
|
|
65
|
+
/** Step number, or "result" for the terminal gate. */
|
|
66
|
+
readonly at: number | "result";
|
|
67
|
+
readonly gate: RuntimeGate;
|
|
68
|
+
readonly ok: boolean;
|
|
69
|
+
readonly output: string;
|
|
70
|
+
}
|
|
71
|
+
export interface SkillRunReport {
|
|
72
|
+
readonly results: readonly GateRunResult[];
|
|
73
|
+
/** Where the ladder short-circuited, or null when every gate passed. */
|
|
74
|
+
readonly blockedAt: number | "result" | null;
|
|
75
|
+
readonly ok: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Run the gate ladder: step gates in order (short-circuiting on the first
|
|
79
|
+
* failure), then the result gate. Mirrors a Sequence behavior tree —
|
|
80
|
+
* ordered AND with short-circuit on FAILURE.
|
|
81
|
+
*/
|
|
82
|
+
export declare function runSkillGates(gates: SkillGates, cwd: string): SkillRunReport;
|
|
83
|
+
/** Record the skill the agent is currently executing. */
|
|
84
|
+
export declare function setActiveSkill(cwd: string, skillPath: string): void;
|
|
85
|
+
/** Clear the active-skill marker (the skill finished). */
|
|
86
|
+
export declare function clearActiveSkill(cwd: string): void;
|
|
87
|
+
/** The path of the active skill, or null when none is in progress. */
|
|
88
|
+
export declare function readActiveSkill(cwd: string): string | null;
|
|
89
|
+
export interface StopDecision {
|
|
90
|
+
/** Whether the agent may stop (true) or must keep working (false). */
|
|
91
|
+
readonly allow: boolean;
|
|
92
|
+
/** Message for the user (allow) or fed back to the model (block). */
|
|
93
|
+
readonly message: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Stop-hook decision. If a skill is active and declares a result gate, run it:
|
|
97
|
+
* allow the stop only when the gate passes; otherwise block and tell the model
|
|
98
|
+
* what to fix. With no active skill (or no result gate), always allow.
|
|
99
|
+
*/
|
|
100
|
+
export declare function evaluateStopHook(cwd: string): StopDecision;
|
|
101
|
+
//# sourceMappingURL=skill-runtime.d.ts.map
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles — Skill runtime: the deterministic gate ladder.
|
|
4
|
+
*
|
|
5
|
+
* Parses the `vigiles:gate` / `vigiles:result` markers a compiled SKILL.md
|
|
6
|
+
* carries (emitted by compileSkill) and *executes* them: run each step's gate
|
|
7
|
+
* in document order, short-circuit on the first failure (Railway error track),
|
|
8
|
+
* then run the terminal result gate. This is the deterministic spine of the
|
|
9
|
+
* skill driver — the part that turns the markers from documentation into
|
|
10
|
+
* enforcement.
|
|
11
|
+
*
|
|
12
|
+
* Scope (v0): this runs the GATES only. It does not drive the LLM through the
|
|
13
|
+
* prose steps one at a time — that needs a live harness and is a later phase.
|
|
14
|
+
* `retry:N` is parsed and surfaced but executed once here: re-running a
|
|
15
|
+
* deterministic gate without a model in the loop to fix the step yields the
|
|
16
|
+
* same result. Retry is meaningful only once the model re-does the step
|
|
17
|
+
* between attempts.
|
|
18
|
+
*
|
|
19
|
+
* Safety: this executes the gate commands the skill author declared (e.g.
|
|
20
|
+
* `npm test`, `validate.py`) via an explicit, user-invoked command
|
|
21
|
+
* (`vigiles run-skill`). It is not a silent hook and runs nothing the spec
|
|
22
|
+
* didn't declare as a gate.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.parseSkillGates = parseSkillGates;
|
|
26
|
+
exports.detectProjectCommand = detectProjectCommand;
|
|
27
|
+
exports.runGate = runGate;
|
|
28
|
+
exports.gateLabel = gateLabel;
|
|
29
|
+
exports.runSkillGates = runSkillGates;
|
|
30
|
+
exports.setActiveSkill = setActiveSkill;
|
|
31
|
+
exports.clearActiveSkill = clearActiveSkill;
|
|
32
|
+
exports.readActiveSkill = readActiveSkill;
|
|
33
|
+
exports.evaluateStopHook = evaluateStopHook;
|
|
34
|
+
const node_child_process_1 = require("node:child_process");
|
|
35
|
+
const node_fs_1 = require("node:fs");
|
|
36
|
+
const node_path_1 = require("node:path");
|
|
37
|
+
const STEP_RE = /^###\s+Step\s+(\d+)/;
|
|
38
|
+
const GATE_CMD_RE = /<!--\s*vigiles:gate\s+"([^"]*)"(?:\s+retry:(\d+))?\s*-->/;
|
|
39
|
+
const GATE_FILE_RE = /<!--\s*vigiles:gate\s+file:(\S+)\s*-->/;
|
|
40
|
+
const GATE_ROLE_RE = /<!--\s*vigiles:gate\s+role:(\w+)(?:\s+retry:(\d+))?\s*-->/;
|
|
41
|
+
const RESULT_CMD_RE = /<!--\s*vigiles:result\s+"([^"]*)"\s*-->/;
|
|
42
|
+
const RESULT_FILE_RE = /<!--\s*vigiles:result\s+file:(\S+)\s*-->/;
|
|
43
|
+
const RESULT_ROLE_RE = /<!--\s*vigiles:result\s+role:(\w+)\s*-->/;
|
|
44
|
+
/** Parse a single line into a gate, or null if it carries none. */
|
|
45
|
+
function parseGateLine(line) {
|
|
46
|
+
const cmd = GATE_CMD_RE.exec(line);
|
|
47
|
+
if (cmd) {
|
|
48
|
+
return { kind: "cmd", command: cmd[1], retry: cmd[2] ? Number(cmd[2]) : 1 };
|
|
49
|
+
}
|
|
50
|
+
const role = GATE_ROLE_RE.exec(line);
|
|
51
|
+
if (role) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "role",
|
|
54
|
+
role: role[1],
|
|
55
|
+
retry: role[2] ? Number(role[2]) : 1,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const fileM = GATE_FILE_RE.exec(line);
|
|
59
|
+
if (fileM)
|
|
60
|
+
return { kind: "file", path: fileM[1], retry: 1 };
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** Parse the terminal result gate from a line, or null. */
|
|
64
|
+
function parseResultLine(line) {
|
|
65
|
+
const cmd = RESULT_CMD_RE.exec(line);
|
|
66
|
+
if (cmd)
|
|
67
|
+
return { kind: "cmd", command: cmd[1], retry: 1 };
|
|
68
|
+
const role = RESULT_ROLE_RE.exec(line);
|
|
69
|
+
if (role)
|
|
70
|
+
return { kind: "role", role: role[1], retry: 1 };
|
|
71
|
+
const fileM = RESULT_FILE_RE.exec(line);
|
|
72
|
+
if (fileM)
|
|
73
|
+
return { kind: "file", path: fileM[1], retry: 1 };
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/** Extract the step gates and result gate from a compiled SKILL.md. */
|
|
77
|
+
function parseSkillGates(markdown) {
|
|
78
|
+
const steps = [];
|
|
79
|
+
let result;
|
|
80
|
+
let currentStep = 0;
|
|
81
|
+
for (const line of markdown.split("\n")) {
|
|
82
|
+
const stepMatch = STEP_RE.exec(line);
|
|
83
|
+
if (stepMatch) {
|
|
84
|
+
currentStep = Number(stepMatch[1]);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const resultGate = parseResultLine(line);
|
|
88
|
+
if (resultGate) {
|
|
89
|
+
result = resultGate;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const gate = parseGateLine(line);
|
|
93
|
+
if (gate)
|
|
94
|
+
steps.push({ step: currentStep, gate });
|
|
95
|
+
}
|
|
96
|
+
return { steps, result };
|
|
97
|
+
}
|
|
98
|
+
/** Execute a shell command in `cwd`; exit 0 = pass, capturing output. */
|
|
99
|
+
function execCommand(command, cwd) {
|
|
100
|
+
try {
|
|
101
|
+
const out = (0, node_child_process_1.execSync)(command, {
|
|
102
|
+
cwd,
|
|
103
|
+
encoding: "utf-8",
|
|
104
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
105
|
+
});
|
|
106
|
+
return { ok: true, output: out.trim() };
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
const err = e;
|
|
110
|
+
const output = [err.stdout, err.stderr].filter(Boolean).join("\n").trim();
|
|
111
|
+
return { ok: false, output };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const NPM_FOR_ROLE = {
|
|
115
|
+
test: "npm test",
|
|
116
|
+
build: "npm run build",
|
|
117
|
+
lint: "npm run lint",
|
|
118
|
+
};
|
|
119
|
+
/** Detect the npm command for a role from package.json scripts, or null. */
|
|
120
|
+
function detectNpmCommand(role, cwd) {
|
|
121
|
+
const pkg = (0, node_path_1.resolve)(cwd, "package.json");
|
|
122
|
+
if (!(0, node_fs_1.existsSync)(pkg))
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
const scripts = JSON.parse((0, node_fs_1.readFileSync)(pkg, "utf-8")).scripts ?? {};
|
|
126
|
+
return scripts[role] ? (NPM_FOR_ROLE[role] ?? null) : null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Non-npm ecosystems: marker files + the command for each role. */
|
|
133
|
+
const ECOSYSTEMS = [
|
|
134
|
+
{
|
|
135
|
+
markers: ["pyproject.toml", "setup.cfg"],
|
|
136
|
+
commands: { test: "pytest", lint: "ruff check ." },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
markers: ["Cargo.toml"],
|
|
140
|
+
commands: {
|
|
141
|
+
test: "cargo test",
|
|
142
|
+
build: "cargo build",
|
|
143
|
+
lint: "cargo clippy",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
markers: ["go.mod"],
|
|
148
|
+
commands: { test: "go test ./...", build: "go build ./..." },
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
/**
|
|
152
|
+
* Resolve a project role (test/build/lint) to the host project's real command,
|
|
153
|
+
* detected from its ecosystem. Returns null when no command can be found —
|
|
154
|
+
* which the caller surfaces as a failed gate rather than a silent pass.
|
|
155
|
+
*/
|
|
156
|
+
function detectProjectCommand(role, cwd) {
|
|
157
|
+
const npm = detectNpmCommand(role, cwd);
|
|
158
|
+
if (npm)
|
|
159
|
+
return npm;
|
|
160
|
+
for (const eco of ECOSYSTEMS) {
|
|
161
|
+
const present = eco.markers.some((m) => (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, m)));
|
|
162
|
+
const command = eco.commands[role];
|
|
163
|
+
if (present && command)
|
|
164
|
+
return command;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Run one gate against `cwd`: a command (exit 0 = pass), a file existence, or
|
|
170
|
+
* a project role resolved to the host project's command.
|
|
171
|
+
*/
|
|
172
|
+
function runGate(gate, cwd) {
|
|
173
|
+
if (gate.kind === "file") {
|
|
174
|
+
const there = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, gate.path));
|
|
175
|
+
return { ok: there, output: there ? "" : `${gate.path} not found` };
|
|
176
|
+
}
|
|
177
|
+
if (gate.kind === "role") {
|
|
178
|
+
const command = detectProjectCommand(gate.role, cwd);
|
|
179
|
+
if (!command) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
output: `No ${gate.role} command detected for this project`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return execCommand(command, cwd);
|
|
186
|
+
}
|
|
187
|
+
return execCommand(gate.command, cwd);
|
|
188
|
+
}
|
|
189
|
+
/** Human-readable label for a gate (for reports and hook messages). */
|
|
190
|
+
function gateLabel(gate) {
|
|
191
|
+
if (gate.kind === "cmd")
|
|
192
|
+
return `\`${gate.command}\``;
|
|
193
|
+
if (gate.kind === "role")
|
|
194
|
+
return `the project's ${gate.role} command`;
|
|
195
|
+
return `${gate.path} exists`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Run the gate ladder: step gates in order (short-circuiting on the first
|
|
199
|
+
* failure), then the result gate. Mirrors a Sequence behavior tree —
|
|
200
|
+
* ordered AND with short-circuit on FAILURE.
|
|
201
|
+
*/
|
|
202
|
+
function runSkillGates(gates, cwd) {
|
|
203
|
+
const results = [];
|
|
204
|
+
for (const { step, gate } of gates.steps) {
|
|
205
|
+
const r = runGate(gate, cwd);
|
|
206
|
+
results.push({ at: step, gate, ok: r.ok, output: r.output });
|
|
207
|
+
if (!r.ok)
|
|
208
|
+
return { results, blockedAt: step, ok: false };
|
|
209
|
+
}
|
|
210
|
+
if (gates.result) {
|
|
211
|
+
const r = runGate(gates.result, cwd);
|
|
212
|
+
results.push({
|
|
213
|
+
at: "result",
|
|
214
|
+
gate: gates.result,
|
|
215
|
+
ok: r.ok,
|
|
216
|
+
output: r.output,
|
|
217
|
+
});
|
|
218
|
+
if (!r.ok)
|
|
219
|
+
return { results, blockedAt: "result", ok: false };
|
|
220
|
+
}
|
|
221
|
+
return { results, blockedAt: null, ok: true };
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Stop-hook enforcement
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
//
|
|
227
|
+
// A skill is "active" while the agent is executing it. The Stop hook then runs
|
|
228
|
+
// the active skill's result gate and blocks completion until it passes — so the
|
|
229
|
+
// agent cannot declare a skill done until its result is deterministically
|
|
230
|
+
// proven. Which skill is active is tracked in `.vigiles/active-skill.json`
|
|
231
|
+
// (Claude Code hooks don't surface the active skill, so we record it). Wiring
|
|
232
|
+
// `skill-start` to fire automatically is the integration step; the decision
|
|
233
|
+
// logic below is harness-agnostic and fully testable.
|
|
234
|
+
const ACTIVE_PATH = ".vigiles/active-skill.json";
|
|
235
|
+
/** Record the skill the agent is currently executing. */
|
|
236
|
+
function setActiveSkill(cwd, skillPath) {
|
|
237
|
+
const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
|
|
238
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
|
|
239
|
+
(0, node_fs_1.writeFileSync)(p, JSON.stringify({ skill: skillPath }) + "\n");
|
|
240
|
+
}
|
|
241
|
+
/** Clear the active-skill marker (the skill finished). */
|
|
242
|
+
function clearActiveSkill(cwd) {
|
|
243
|
+
const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
|
|
244
|
+
if ((0, node_fs_1.existsSync)(p))
|
|
245
|
+
(0, node_fs_1.rmSync)(p);
|
|
246
|
+
}
|
|
247
|
+
/** The path of the active skill, or null when none is in progress. */
|
|
248
|
+
function readActiveSkill(cwd) {
|
|
249
|
+
const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
|
|
250
|
+
if (!(0, node_fs_1.existsSync)(p))
|
|
251
|
+
return null;
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
|
|
254
|
+
return typeof parsed.skill === "string" ? parsed.skill : null;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Stop-hook decision. If a skill is active and declares a result gate, run it:
|
|
262
|
+
* allow the stop only when the gate passes; otherwise block and tell the model
|
|
263
|
+
* what to fix. With no active skill (or no result gate), always allow.
|
|
264
|
+
*/
|
|
265
|
+
function evaluateStopHook(cwd) {
|
|
266
|
+
const skillPath = readActiveSkill(cwd);
|
|
267
|
+
if (!skillPath)
|
|
268
|
+
return { allow: true, message: "" };
|
|
269
|
+
const full = (0, node_path_1.resolve)(cwd, skillPath);
|
|
270
|
+
if (!(0, node_fs_1.existsSync)(full))
|
|
271
|
+
return { allow: true, message: "" };
|
|
272
|
+
const gates = parseSkillGates((0, node_fs_1.readFileSync)(full, "utf-8"));
|
|
273
|
+
if (!gates.result)
|
|
274
|
+
return { allow: true, message: "" };
|
|
275
|
+
const outcome = runGate(gates.result, cwd);
|
|
276
|
+
const desc = gateLabel(gates.result);
|
|
277
|
+
if (outcome.ok) {
|
|
278
|
+
return {
|
|
279
|
+
allow: true,
|
|
280
|
+
message: `✓ ${skillPath}: result gate ${desc} passed.`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const tail = outcome.output ? `\n${outcome.output}` : "";
|
|
284
|
+
return {
|
|
285
|
+
allow: false,
|
|
286
|
+
message: `Skill "${skillPath}" is not done: result gate ${desc} failed. Fix it, then finish.${tail}`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=skill-runtime.js.map
|