vigiles 2.6.0 → 4.0.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +11 -2
- package/README.md +76 -129
- package/action.yml +144 -8
- package/dist/action-gate.d.ts +1 -1
- package/dist/action-gate.js +1 -1
- package/dist/adapter-conformance.d.ts +30 -0
- package/dist/adapter-conformance.js +153 -0
- package/dist/adapter-registry.d.ts +42 -0
- package/dist/adapter-registry.js +55 -0
- package/dist/adapter.d.ts +26 -0
- package/dist/adapter.js +16 -0
- package/dist/adapters/claude-code/adapter.d.ts +3 -0
- package/dist/adapters/claude-code/adapter.js +46 -0
- package/dist/{agent-result.d.ts → adapters/claude-code/agent-result.d.ts} +1 -1
- package/dist/adapters/claude-code/dialect.d.ts +13 -0
- package/dist/adapters/claude-code/dialect.js +51 -0
- package/dist/adapters/claude-code/egress-entry.d.ts +2 -0
- package/dist/adapters/claude-code/egress-entry.js +115 -0
- package/dist/adapters/claude-code/egress.d.ts +114 -0
- package/dist/adapters/claude-code/egress.js +276 -0
- package/dist/{eval-cache.d.ts → adapters/claude-code/eval-cache.d.ts} +1 -1
- package/dist/{eval-cache.js → adapters/claude-code/eval-cache.js} +1 -1
- package/dist/{eval.d.ts → adapters/claude-code/eval.d.ts} +28 -4
- package/dist/{eval.js → adapters/claude-code/eval.js} +61 -25
- package/dist/{harness-test.d.ts → adapters/claude-code/harness-test.d.ts} +33 -37
- package/dist/{harness-test.js → adapters/claude-code/harness-test.js} +124 -43
- package/dist/adapters/claude-code/hook-protocol.d.ts +10 -0
- package/dist/adapters/claude-code/hook-protocol.js +10 -0
- package/dist/adapters/claude-code/layout.d.ts +8 -0
- package/dist/adapters/claude-code/layout.js +18 -0
- package/dist/{mock-model.d.ts → adapters/claude-code/mock-model.d.ts} +2 -24
- package/dist/adapters/claude-code/model-mock.d.ts +11 -0
- package/dist/adapters/claude-code/model-mock.js +10 -0
- package/dist/adapters/claude-code/plugin-loader.d.ts +25 -0
- package/dist/adapters/claude-code/plugin-loader.js +19 -0
- package/dist/{run-hook.d.ts → adapters/claude-code/run-hook.d.ts} +50 -3
- package/dist/{run-hook.js → adapters/claude-code/run-hook.js} +178 -18
- package/dist/adapters/claude-code/run-scripts.d.ts +52 -0
- package/dist/adapters/claude-code/run-scripts.js +150 -0
- package/dist/adapters/claude-code/runtime.d.ts +16 -0
- package/dist/adapters/claude-code/runtime.js +39 -0
- package/dist/{sandbox.d.ts → adapters/claude-code/sandbox.d.ts} +10 -0
- package/dist/{sandbox.js → adapters/claude-code/sandbox.js} +5 -3
- package/dist/{skill-driver.d.ts → adapters/claude-code/skill-driver.d.ts} +1 -1
- package/dist/adapters/codex/adapter.d.ts +3 -0
- package/dist/adapters/codex/adapter.js +49 -0
- package/dist/adapters/codex/dialect.d.ts +10 -0
- package/dist/adapters/codex/dialect.js +30 -0
- package/dist/adapters/codex/driver.d.ts +20 -0
- package/dist/adapters/codex/driver.js +89 -0
- package/dist/adapters/codex/hook-protocol.d.ts +10 -0
- package/dist/adapters/codex/hook-protocol.js +18 -0
- package/dist/adapters/codex/layout.d.ts +16 -0
- package/dist/adapters/codex/layout.js +18 -0
- package/dist/adapters/codex/mock-model.d.ts +52 -0
- package/dist/adapters/codex/mock-model.js +210 -0
- package/dist/adapters/codex/model-mock.d.ts +11 -0
- package/dist/adapters/codex/model-mock.js +10 -0
- package/dist/adapters/codex/runtime.d.ts +38 -0
- package/dist/adapters/codex/runtime.js +62 -0
- package/dist/adapters/opencode/adapter.d.ts +3 -0
- package/dist/adapters/opencode/adapter.js +48 -0
- package/dist/adapters/opencode/dialect.d.ts +10 -0
- package/dist/adapters/opencode/dialect.js +36 -0
- package/dist/adapters/opencode/layout.d.ts +9 -0
- package/dist/adapters/opencode/layout.js +25 -0
- package/dist/adapters/opencode/model-mock.d.ts +10 -0
- package/dist/adapters/opencode/model-mock.js +10 -0
- package/dist/adapters/opencode/runtime.d.ts +9 -0
- package/dist/adapters/opencode/runtime.js +21 -0
- package/dist/claude-code.d.ts +8 -2
- package/dist/claude-code.js +8 -2
- package/dist/cli-flags.d.ts +22 -0
- package/dist/cli-flags.js +38 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +670 -245
- package/dist/codex.d.ts +20 -0
- package/dist/codex.js +36 -0
- package/dist/community-skills.d.ts +1 -1
- package/dist/community-skills.js +2 -2
- package/dist/core/adapter.d.ts +88 -0
- package/dist/core/adapter.js +3 -0
- package/dist/{compile.d.ts → core/compile.d.ts} +11 -2
- package/dist/{compile.js → core/compile.js} +68 -53
- package/dist/core/compose.d.ts +79 -0
- package/dist/core/compose.js +145 -0
- package/dist/core/dialect.d.ts +51 -0
- package/dist/core/dialect.js +3 -0
- package/dist/core/harness-driver.d.ts +134 -0
- package/dist/core/harness-driver.js +3 -0
- package/dist/core/hook-protocol.d.ts +28 -0
- package/dist/core/hook-protocol.js +3 -0
- package/dist/core/layout.d.ts +43 -0
- package/dist/core/layout.js +3 -0
- package/dist/core/model-mock.d.ts +27 -0
- package/dist/core/model-mock.js +3 -0
- package/dist/core/refs.d.ts +62 -0
- package/dist/{refs.js → core/refs.js} +49 -23
- package/dist/core/runtime.d.ts +38 -0
- package/dist/core/runtime.js +3 -0
- package/dist/{types.d.ts → core/types.d.ts} +29 -0
- package/dist/{validate.d.ts → core/validate.d.ts} +1 -1
- package/dist/{validate.js → core/validate.js} +41 -4
- package/dist/e2e.d.ts +19 -0
- package/dist/e2e.js +39 -0
- package/dist/harness-assert.d.ts +29 -14
- package/dist/harness-assert.js +37 -10
- package/dist/integration.d.ts +16 -0
- package/dist/integration.js +32 -0
- package/dist/leaderboard.d.ts +33 -0
- package/dist/leaderboard.js +107 -0
- package/dist/linting.d.ts +2 -2
- package/dist/linting.js +2 -2
- package/dist/plugin-loader.d.ts +6 -3
- package/dist/plugin-loader.js +101 -49
- package/dist/scan.d.ts +49 -0
- package/dist/scan.js +177 -0
- package/dist/setup-plan.d.ts +91 -0
- package/dist/setup-plan.js +160 -0
- package/dist/skill-test.d.ts +1 -1
- package/dist/skill-test.js +1 -1
- package/dist/test-coverage.d.ts +71 -0
- package/dist/test-coverage.js +228 -0
- package/dist/testing.d.ts +3 -3
- package/dist/testing.js +3 -3
- package/dist/unit.d.ts +17 -0
- package/dist/unit.js +36 -0
- package/hooks/refs-nudge.sh +24 -0
- package/package.json +30 -15
- package/skills/edit-spec/SKILL.md +131 -0
- package/skills/generate-rule/SKILL.md +64 -0
- package/skills/linter-docs/clippy.md +241 -0
- package/skills/linter-docs/eslint.md +384 -0
- package/skills/linter-docs/pylint.md +288 -0
- package/skills/linter-docs/rubocop.md +277 -0
- package/skills/linter-docs/ruff.md +187 -0
- package/skills/linter-docs/stylelint.md +247 -0
- package/skills/migrate-to-spec/SKILL.md +126 -0
- package/skills/strengthen/SKILL.md +168 -0
- package/skills/test-harness/SKILL.md +157 -0
- package/dist/action.d.ts +0 -7
- package/dist/action.js +0 -180
- package/dist/refs.d.ts +0 -44
- package/dist/run-scripts.d.ts +0 -20
- package/dist/run-scripts.js +0 -70
- /package/dist/{agent-result.js → adapters/claude-code/agent-result.js} +0 -0
- /package/dist/{agent-runtime.d.ts → adapters/claude-code/agent-runtime.d.ts} +0 -0
- /package/dist/{agent-runtime.js → adapters/claude-code/agent-runtime.js} +0 -0
- /package/dist/{egress-proxy.d.ts → adapters/claude-code/egress-proxy.d.ts} +0 -0
- /package/dist/{egress-proxy.js → adapters/claude-code/egress-proxy.js} +0 -0
- /package/dist/{eval-baseline.d.ts → adapters/claude-code/eval-baseline.d.ts} +0 -0
- /package/dist/{eval-baseline.js → adapters/claude-code/eval-baseline.js} +0 -0
- /package/dist/{judge.d.ts → adapters/claude-code/judge.d.ts} +0 -0
- /package/dist/{judge.js → adapters/claude-code/judge.js} +0 -0
- /package/dist/{mock-entry.d.ts → adapters/claude-code/mock-entry.d.ts} +0 -0
- /package/dist/{mock-entry.js → adapters/claude-code/mock-entry.js} +0 -0
- /package/dist/{mock-model.js → adapters/claude-code/mock-model.js} +0 -0
- /package/dist/{skill-driver.js → adapters/claude-code/skill-driver.js} +0 -0
- /package/dist/{skill-runtime.d.ts → adapters/claude-code/skill-runtime.d.ts} +0 -0
- /package/dist/{skill-runtime.js → adapters/claude-code/skill-runtime.js} +0 -0
- /package/dist/{stats.d.ts → adapters/claude-code/stats.d.ts} +0 -0
- /package/dist/{stats.js → adapters/claude-code/stats.js} +0 -0
- /package/dist/{compile-generator.d.ts → core/compile-generator.d.ts} +0 -0
- /package/dist/{compile-generator.js → core/compile-generator.js} +0 -0
- /package/dist/{coverage.d.ts → core/coverage.d.ts} +0 -0
- /package/dist/{coverage.js → core/coverage.js} +0 -0
- /package/dist/{doc-refs.d.ts → core/doc-refs.d.ts} +0 -0
- /package/dist/{doc-refs.js → core/doc-refs.js} +0 -0
- /package/dist/{evolve.d.ts → core/evolve.d.ts} +0 -0
- /package/dist/{evolve.js → core/evolve.js} +0 -0
- /package/dist/{frontmatter.d.ts → core/frontmatter.d.ts} +0 -0
- /package/dist/{frontmatter.js → core/frontmatter.js} +0 -0
- /package/dist/{generate-schema.d.ts → core/generate-schema.d.ts} +0 -0
- /package/dist/{generate-schema.js → core/generate-schema.js} +0 -0
- /package/dist/{generate-types.d.ts → core/generate-types.d.ts} +0 -0
- /package/dist/{generate-types.js → core/generate-types.js} +0 -0
- /package/dist/{hash.d.ts → core/hash.d.ts} +0 -0
- /package/dist/{hash.js → core/hash.js} +0 -0
- /package/dist/{inline.d.ts → core/inline.d.ts} +0 -0
- /package/dist/{inline.js → core/inline.js} +0 -0
- /package/dist/{integrity.d.ts → core/integrity.d.ts} +0 -0
- /package/dist/{integrity.js → core/integrity.js} +0 -0
- /package/dist/{linters.d.ts → core/linters.d.ts} +0 -0
- /package/dist/{linters.js → core/linters.js} +0 -0
- /package/dist/{mcp.d.ts → core/mcp.d.ts} +0 -0
- /package/dist/{mcp.js → core/mcp.js} +0 -0
- /package/dist/{orphans.d.ts → core/orphans.d.ts} +0 -0
- /package/dist/{orphans.js → core/orphans.js} +0 -0
- /package/dist/{proofs.d.ts → core/proofs.d.ts} +0 -0
- /package/dist/{proofs.js → core/proofs.js} +0 -0
- /package/dist/{session.d.ts → core/session.d.ts} +0 -0
- /package/dist/{session.js → core/session.js} +0 -0
- /package/dist/{sidecar.d.ts → core/sidecar.d.ts} +0 -0
- /package/dist/{sidecar.js → core/sidecar.js} +0 -0
- /package/dist/{spec.d.ts → core/spec.d.ts} +0 -0
- /package/dist/{spec.js → core/spec.js} +0 -0
- /package/dist/{symbols.d.ts → core/symbols.d.ts} +0 -0
- /package/dist/{symbols.js → core/symbols.js} +0 -0
- /package/dist/{test-utils.d.ts → core/test-utils.d.ts} +0 -0
- /package/dist/{test-utils.js → core/test-utils.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/{.claude-plugin/hooks → hooks}/post-edit.sh +0 -0
- /package/{.claude-plugin/hooks → hooks}/pre-edit.sh +0 -0
- /package/{.claude-plugin/hooks → hooks}/session-start.sh +0 -0
package/dist/cli.js
CHANGED
|
@@ -6,34 +6,42 @@
|
|
|
6
6
|
* Commands:
|
|
7
7
|
* vigiles init — scaffold a spec from scratch
|
|
8
8
|
* vigiles compile — compile .spec.ts → .md with linter verification
|
|
9
|
-
* vigiles
|
|
9
|
+
* vigiles lint — verify hashes, report coverage, detect duplicates
|
|
10
10
|
* vigiles generate-types — emit .d.ts with types from project state
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
const node_fs_1 = require("node:fs");
|
|
14
14
|
const node_path_1 = require("node:path");
|
|
15
15
|
const glob_1 = require("glob");
|
|
16
|
-
const generate_types_js_1 = require("./generate-types.js");
|
|
17
|
-
const validate_js_1 = require("./validate.js");
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
16
|
+
const generate_types_js_1 = require("./core/generate-types.js");
|
|
17
|
+
const validate_js_1 = require("./core/validate.js");
|
|
18
|
+
const cli_flags_js_1 = require("./cli-flags.js");
|
|
19
|
+
const setup_plan_js_1 = require("./setup-plan.js");
|
|
20
|
+
const types_js_1 = require("./core/types.js");
|
|
21
|
+
const test_coverage_js_1 = require("./test-coverage.js");
|
|
22
|
+
const scan_js_1 = require("./scan.js");
|
|
23
|
+
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
24
|
+
const leaderboard_js_1 = require("./leaderboard.js");
|
|
25
|
+
const compile_js_1 = require("./core/compile.js");
|
|
26
|
+
const dialect_js_1 = require("./adapters/claude-code/dialect.js");
|
|
27
|
+
const proofs_js_1 = require("./core/proofs.js");
|
|
28
|
+
const inline_js_1 = require("./core/inline.js");
|
|
29
|
+
const frontmatter_js_1 = require("./core/frontmatter.js");
|
|
30
|
+
const generate_schema_js_1 = require("./core/generate-schema.js");
|
|
31
|
+
const compose_js_1 = require("./core/compose.js");
|
|
32
|
+
const compile_generator_js_1 = require("./core/compile-generator.js");
|
|
25
33
|
const action_gate_js_1 = require("./action-gate.js");
|
|
26
|
-
const agent_runtime_js_1 = require("./agent-runtime.js");
|
|
27
|
-
const refs_js_1 = require("./refs.js");
|
|
28
|
-
const mcp_js_1 = require("./mcp.js");
|
|
29
|
-
const skill_runtime_js_1 = require("./skill-runtime.js");
|
|
30
|
-
const linters_js_1 = require("./linters.js");
|
|
31
|
-
const harness_test_js_1 = require("./harness-test.js");
|
|
32
|
-
const run_scripts_js_1 = require("./run-scripts.js");
|
|
33
|
-
const integrity_js_1 = require("./integrity.js");
|
|
34
|
-
const coverage_js_1 = require("./coverage.js");
|
|
35
|
-
const orphans_js_1 = require("./orphans.js");
|
|
36
|
-
const doc_refs_js_1 = require("./doc-refs.js");
|
|
34
|
+
const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
|
|
35
|
+
const refs_js_1 = require("./core/refs.js");
|
|
36
|
+
const mcp_js_1 = require("./core/mcp.js");
|
|
37
|
+
const skill_runtime_js_1 = require("./adapters/claude-code/skill-runtime.js");
|
|
38
|
+
const linters_js_1 = require("./core/linters.js");
|
|
39
|
+
const harness_test_js_1 = require("./adapters/claude-code/harness-test.js");
|
|
40
|
+
const run_scripts_js_1 = require("./adapters/claude-code/run-scripts.js");
|
|
41
|
+
const integrity_js_1 = require("./core/integrity.js");
|
|
42
|
+
const coverage_js_1 = require("./core/coverage.js");
|
|
43
|
+
const orphans_js_1 = require("./core/orphans.js");
|
|
44
|
+
const doc_refs_js_1 = require("./core/doc-refs.js");
|
|
37
45
|
// ---------------------------------------------------------------------------
|
|
38
46
|
// Constants
|
|
39
47
|
// ---------------------------------------------------------------------------
|
|
@@ -44,7 +52,11 @@ const IGNORE_NODE_MODULES = ["node_modules/**"];
|
|
|
44
52
|
function findSpecs(pattern) {
|
|
45
53
|
const glob = pattern ?? "**/*.md.spec.ts";
|
|
46
54
|
return (0, glob_1.globSync)(glob, {
|
|
47
|
-
|
|
55
|
+
// `dot: true` so specs that live in a sync tool's source slot (e.g.
|
|
56
|
+
// `.ruler/AGENTS.md.spec.ts`, the redirect target) are discovered by
|
|
57
|
+
// compile/audit/the recompile hook — not just root-level specs.
|
|
58
|
+
dot: true,
|
|
59
|
+
ignore: [...IGNORE_NODE_MODULES, "dist/**", ".git/**"],
|
|
48
60
|
cwd: process.cwd(),
|
|
49
61
|
});
|
|
50
62
|
}
|
|
@@ -133,6 +145,7 @@ function compileClaudeToFile(spec, specPath, config) {
|
|
|
133
145
|
const { markdown, errors, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
|
|
134
146
|
basePath,
|
|
135
147
|
specFile: specPath,
|
|
148
|
+
dialect: dialect_js_1.claudeCodeDialect,
|
|
136
149
|
maxRules: config.maxRules,
|
|
137
150
|
maxTokens: config.maxTokens,
|
|
138
151
|
maxSectionLines: config.maxSectionLines,
|
|
@@ -168,6 +181,9 @@ function compileSkillToFile(spec, specPath) {
|
|
|
168
181
|
const { markdown, errors } = (0, compile_js_1.compileSkill)(spec, {
|
|
169
182
|
basePath: process.cwd(),
|
|
170
183
|
specFile: specPath,
|
|
184
|
+
// Pick the SKILL.md frontmatter profile from the detected harness — a Codex
|
|
185
|
+
// repo gets a minimal (name + description) SKILL.md; CC gets the full set.
|
|
186
|
+
dialect: (0, adapter_registry_js_1.detectAdapter)(process.cwd()).dialect,
|
|
171
187
|
});
|
|
172
188
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
|
|
173
189
|
if (errors.length === 0) {
|
|
@@ -184,6 +200,7 @@ function compileAgentToFile(spec, specPath) {
|
|
|
184
200
|
const { markdown, errors } = (0, compile_js_1.compileAgent)(spec, {
|
|
185
201
|
basePath: process.cwd(),
|
|
186
202
|
specFile: specPath,
|
|
203
|
+
dialect: (0, adapter_registry_js_1.detectAdapter)(process.cwd()).dialect,
|
|
187
204
|
});
|
|
188
205
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
|
|
189
206
|
if (errors.length === 0) {
|
|
@@ -388,7 +405,7 @@ async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
|
|
|
388
405
|
const allSpecs = findSpecs();
|
|
389
406
|
// If audit was invoked with explicit file arguments, only scan the specs
|
|
390
407
|
// for those files — otherwise an unrelated duplicate elsewhere in the
|
|
391
|
-
// repo would fail a targeted CI check (e.g. `vigiles
|
|
408
|
+
// repo would fail a targeted CI check (e.g. `vigiles lint path/foo.md`).
|
|
392
409
|
//
|
|
393
410
|
// Resolve each requested file to its real source spec by reading the
|
|
394
411
|
// compiled-from header. Multi-target projects compile one spec to
|
|
@@ -541,6 +558,7 @@ function auditExitCode(report) {
|
|
|
541
558
|
report.frontmatterErrors > 0 ||
|
|
542
559
|
report.integrityErrors > 0 ||
|
|
543
560
|
report.coverageErrors > 0 ||
|
|
561
|
+
report.untestedErrors > 0 ||
|
|
544
562
|
report.symbolRefErrors > 0 ||
|
|
545
563
|
report.mcpRefErrors > 0)
|
|
546
564
|
return 2;
|
|
@@ -817,6 +835,10 @@ async function audit(restArgs, flags, config) {
|
|
|
817
835
|
console.log(` ${line}`);
|
|
818
836
|
}
|
|
819
837
|
}
|
|
838
|
+
// 7b. Untested-surface check — skills/agents/hooks shipping without a test or
|
|
839
|
+
// eval. Warning by default (a nudge, exit 0); set rules.untested-surface to
|
|
840
|
+
// "error" to gate CI. See src/test-coverage.ts and docs/rules/untested-surface.md.
|
|
841
|
+
const untested = checkUntestedSurfaces(config, silent);
|
|
820
842
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
821
843
|
// is to validate every ref; illustrative blocks opt out via
|
|
822
844
|
// `<!-- vigiles:ignore -->` (single block) or
|
|
@@ -848,6 +870,8 @@ async function audit(restArgs, flags, config) {
|
|
|
848
870
|
integrityErrors,
|
|
849
871
|
coverageErrors,
|
|
850
872
|
orphanCount: orphanReport.orphans.length,
|
|
873
|
+
untestedSurfaces: untested.untested,
|
|
874
|
+
untestedErrors: untested.errors,
|
|
851
875
|
docRefErrors: docRefReport.errors.length,
|
|
852
876
|
symbolRefErrors,
|
|
853
877
|
mcpRefErrors,
|
|
@@ -876,6 +900,8 @@ function printAuditSummary(report) {
|
|
|
876
900
|
parts.push(`${String(report.duplicatePairs)} duplicates`);
|
|
877
901
|
if (report.orphanCount > 0)
|
|
878
902
|
parts.push(`${String(report.orphanCount)} orphan docs`);
|
|
903
|
+
if (report.untestedSurfaces > 0)
|
|
904
|
+
parts.push(`${String(report.untestedSurfaces)} untested surfaces`);
|
|
879
905
|
if (report.docRefErrors > 0)
|
|
880
906
|
parts.push(`${String(report.docRefErrors)} broken doc refs`);
|
|
881
907
|
if (report.symbolRefErrors > 0)
|
|
@@ -968,6 +994,34 @@ function discover(silent = false) {
|
|
|
968
994
|
}
|
|
969
995
|
return { enabled: totalEnabled, documented: totalDocumented };
|
|
970
996
|
}
|
|
997
|
+
/** This package's own version (from the installed package.json). */
|
|
998
|
+
function getVersion() {
|
|
999
|
+
try {
|
|
1000
|
+
// dist/cli.js → ../package.json (the package root).
|
|
1001
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(__dirname, "..", "package.json"), "utf-8"));
|
|
1002
|
+
return pkg.version ?? "unknown";
|
|
1003
|
+
}
|
|
1004
|
+
catch {
|
|
1005
|
+
return "unknown";
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/** The dependency range to pin `vigiles` to (the running CLI's major, e.g.
|
|
1009
|
+
* `^3`). Falls back to `latest` for the unreleased dev placeholder version. */
|
|
1010
|
+
function vigilesDepSpec() {
|
|
1011
|
+
const major = parseInt(getVersion(), 10);
|
|
1012
|
+
return Number.isFinite(major) && major > 0 ? `^${String(major)}` : "latest";
|
|
1013
|
+
}
|
|
1014
|
+
/** True when the file's first line carries a vigiles integrity hash (i.e. it
|
|
1015
|
+
* is a compiled artifact we own, safe to overwrite — not hand-written prose). */
|
|
1016
|
+
function targetHasHash(absPath) {
|
|
1017
|
+
try {
|
|
1018
|
+
const first = (0, node_fs_1.readFileSync)(absPath, "utf-8").split("\n", 1)[0];
|
|
1019
|
+
return first.includes("vigiles:sha256");
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
971
1025
|
function init(args) {
|
|
972
1026
|
const targetFlag = args.find((a) => a.startsWith("--target="));
|
|
973
1027
|
const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
|
|
@@ -976,7 +1030,12 @@ function init(args) {
|
|
|
976
1030
|
console.log(`${specPath} already exists.`);
|
|
977
1031
|
return;
|
|
978
1032
|
}
|
|
979
|
-
|
|
1033
|
+
// The compiled output is derived from the spec FILE path; the spec's `target`
|
|
1034
|
+
// field is the h1 + the name the compiler validates against, so it must be the
|
|
1035
|
+
// bare filename even when the spec lives in a subdir (e.g. a sync tool's
|
|
1036
|
+
// `.ruler/AGENTS.md.spec.ts` source slot → target "AGENTS.md").
|
|
1037
|
+
const targetName = (0, node_path_1.basename)(target);
|
|
1038
|
+
const targetLine = targetName !== "CLAUDE.md" ? `\n target: "${targetName}",` : "";
|
|
980
1039
|
const template = `import { claude, enforce, guidance } from "vigiles/spec";
|
|
981
1040
|
|
|
982
1041
|
export default claude({${targetLine}
|
|
@@ -1010,40 +1069,172 @@ export default claude({${targetLine}
|
|
|
1010
1069
|
},
|
|
1011
1070
|
});
|
|
1012
1071
|
`;
|
|
1013
|
-
|
|
1072
|
+
const specAbs = (0, node_path_1.resolve)(process.cwd(), specPath);
|
|
1073
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(specAbs), { recursive: true });
|
|
1074
|
+
(0, node_fs_1.writeFileSync)(specAbs, template);
|
|
1014
1075
|
console.log(`Created ${specPath} — edit it and run \`vigiles compile\`.`);
|
|
1015
1076
|
}
|
|
1016
1077
|
// ---------------------------------------------------------------------------
|
|
1017
1078
|
// Setup wizard
|
|
1018
1079
|
// ---------------------------------------------------------------------------
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1080
|
+
/** Full GitHub Actions workflow that wires the production `zernie/vigiles@v1`
|
|
1081
|
+
* Action (lint pillar) and, when the test pillar is set up, a deterministic
|
|
1082
|
+
* harness job. */
|
|
1083
|
+
function vigilesWorkflow(plan) {
|
|
1084
|
+
const harness = plan.test
|
|
1085
|
+
? `
|
|
1086
|
+
harness:
|
|
1087
|
+
# Test pillar — run your *.harness.{mjs,ts} tests against the real agent CLI and
|
|
1088
|
+
# a scripted mock model (deterministic, no API key). Drop this job if you only
|
|
1089
|
+
# author runHook unit tests, or keep it for the deterministic tier.
|
|
1090
|
+
runs-on: ubuntu-latest
|
|
1091
|
+
steps:
|
|
1092
|
+
- uses: actions/checkout@v4
|
|
1093
|
+
- uses: actions/setup-node@v4
|
|
1094
|
+
with:
|
|
1095
|
+
node-version: "20"
|
|
1096
|
+
- run: npm install
|
|
1097
|
+
- run: npm i -g @anthropic-ai/claude-code # mock tier needs the binary, no API key
|
|
1098
|
+
- run: npx vigiles test
|
|
1099
|
+
`
|
|
1100
|
+
: "";
|
|
1101
|
+
return `name: vigiles
|
|
1102
|
+
on:
|
|
1103
|
+
pull_request:
|
|
1104
|
+
push:
|
|
1105
|
+
branches: [main]
|
|
1106
|
+
|
|
1107
|
+
permissions:
|
|
1108
|
+
contents: read
|
|
1109
|
+
pull-requests: write # for the sticky PR comment
|
|
1110
|
+
|
|
1111
|
+
jobs:
|
|
1112
|
+
lint:
|
|
1113
|
+
# Lint pillar — verify the references in your instruction files (composite
|
|
1114
|
+
# Action over the published CLI). Posts a sticky PR comment + a \`valid\` output.
|
|
1115
|
+
runs-on: ubuntu-latest
|
|
1116
|
+
steps:
|
|
1117
|
+
- uses: actions/checkout@v4
|
|
1118
|
+
- uses: actions/setup-node@v4
|
|
1119
|
+
with:
|
|
1120
|
+
node-version: "20"
|
|
1121
|
+
- uses: zernie/vigiles@v1
|
|
1122
|
+
${harness}`;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Detect a workflow that drives vigiles through an OLD API — a bare `npx vigiles`
|
|
1126
|
+
* (a no-op help screen in v2+) rather than the `zernie/vigiles@` Action or a real
|
|
1127
|
+
* subcommand (`audit`/`test`/…). Upgrading users whose workflow predates the
|
|
1128
|
+
* subcommand split silently lose CI validation, so we flag it loudly.
|
|
1129
|
+
*/
|
|
1130
|
+
function workflowUsesStaleApi(content) {
|
|
1131
|
+
if (content.includes("zernie/vigiles@"))
|
|
1132
|
+
return false; // uses the Action — fine
|
|
1133
|
+
if (!/\bvigiles\b/.test(content))
|
|
1134
|
+
return false; // not a vigiles workflow
|
|
1135
|
+
const hasModernCmd = /vigiles\s+(lint|test|eval|compile|scan|generate-types|generate-schema|init)\b/.test(content);
|
|
1136
|
+
return !hasModernCmd;
|
|
1137
|
+
}
|
|
1138
|
+
/** Create `.github/workflows/vigiles.yml`. Returns the files it wrote (for the
|
|
1139
|
+
* commit hint). An existing workflow is never clobbered, but a STALE one (old
|
|
1140
|
+
* bare-`npx vigiles` API) is reported loudly instead of silently skipped. */
|
|
1141
|
+
function wireGha(plan) {
|
|
1142
|
+
const dir = (0, node_path_1.resolve)(process.cwd(), ".github", "workflows");
|
|
1143
|
+
const path = (0, node_path_1.resolve)(dir, "vigiles.yml");
|
|
1144
|
+
if ((0, node_fs_1.existsSync)(path)) {
|
|
1145
|
+
const content = (0, node_fs_1.readFileSync)(path, "utf-8");
|
|
1146
|
+
if (workflowUsesStaleApi(content)) {
|
|
1147
|
+
console.log("⚠ .github/workflows/vigiles.yml is STALE — it runs a bare `npx vigiles`,\n" +
|
|
1148
|
+
" which is a no-op help screen now. Replace its run step with the Action +\n" +
|
|
1149
|
+
" the test job (CI is otherwise silently not validating anything):\n" +
|
|
1150
|
+
" - uses: zernie/vigiles@v1 # lint pillar — verify references\n" +
|
|
1151
|
+
" - run: npx vigiles test # pillar 2 — harness tests\n" +
|
|
1152
|
+
" Or delete the file and re-run `vigiles init` to regenerate it.");
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
console.log("✓ .github/workflows/vigiles.yml already exists (up to date)");
|
|
1044
1156
|
}
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1159
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
1160
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
1161
|
+
(0, node_fs_1.writeFileSync)(path, vigilesWorkflow(plan));
|
|
1162
|
+
console.log("✓ Created .github/workflows/vigiles.yml (uses zernie/vigiles@v1)");
|
|
1163
|
+
return [".github/workflows/vigiles.yml"];
|
|
1164
|
+
}
|
|
1165
|
+
const STARTER_HARNESS = `/**
|
|
1166
|
+
* Starter harness test (Pillar 2) — scaffolded by \`vigiles init\`.
|
|
1167
|
+
* Proves a hook actually FIRES, deterministically and with no API key.
|
|
1168
|
+
*
|
|
1169
|
+
* npm i -D vigiles # the testing API this imports
|
|
1170
|
+
* npx vigiles test # or: node vigiles.harness.mjs
|
|
1171
|
+
*
|
|
1172
|
+
* Guide: https://github.com/zernie/vigiles/blob/main/docs/harness-testing.md
|
|
1173
|
+
*/
|
|
1174
|
+
import { runHook } from "vigiles/testing";
|
|
1175
|
+
import assert from "node:assert/strict";
|
|
1176
|
+
|
|
1177
|
+
// EXAMPLE — replace with one of YOUR hooks. This PreToolUse Bash guard blocks a
|
|
1178
|
+
// destructive command; runHook pipes it a fake event and checks the decision.
|
|
1179
|
+
const guard =
|
|
1180
|
+
\`CMD=$(cat | jq -r '.tool_input.command // empty'); \` +
|
|
1181
|
+
\`case "$CMD" in *"rm -rf /"*) echo blocked >&2; exit 2 ;; esac; exit 0\`;
|
|
1182
|
+
|
|
1183
|
+
const blocked = runHook(guard, {
|
|
1184
|
+
hook_event_name: "PreToolUse",
|
|
1185
|
+
tool_name: "Bash",
|
|
1186
|
+
tool_input: { command: "rm -rf / --no-preserve-root" },
|
|
1187
|
+
});
|
|
1188
|
+
assert.ok(blocked.blocked, "guard should block \\\`rm -rf /\\\`");
|
|
1189
|
+
|
|
1190
|
+
const allowed = runHook(guard, {
|
|
1191
|
+
hook_event_name: "PreToolUse",
|
|
1192
|
+
tool_name: "Bash",
|
|
1193
|
+
tool_input: { command: "ls -la" },
|
|
1194
|
+
});
|
|
1195
|
+
assert.ok(!allowed.blocked, "guard should allow a safe command");
|
|
1196
|
+
|
|
1197
|
+
console.log("\\u2713 hook blocks rm -rf / and allows safe commands");
|
|
1198
|
+
`;
|
|
1199
|
+
/** Pillar 2 — scaffold a starter harness test the user adapts to their hooks.
|
|
1200
|
+
* Returns the files it wrote (for the commit hint). */
|
|
1201
|
+
function scaffoldPillar2() {
|
|
1202
|
+
const path = (0, node_path_1.resolve)(process.cwd(), "vigiles.harness.mjs");
|
|
1203
|
+
if ((0, node_fs_1.existsSync)(path)) {
|
|
1204
|
+
console.log("✓ vigiles.harness.mjs already exists");
|
|
1205
|
+
return [];
|
|
1206
|
+
}
|
|
1207
|
+
(0, node_fs_1.writeFileSync)(path, STARTER_HARNESS);
|
|
1208
|
+
console.log("✓ Scaffolded vigiles.harness.mjs — Pillar 2 starter (npx vigiles test)");
|
|
1209
|
+
return ["vigiles.harness.mjs"];
|
|
1210
|
+
}
|
|
1211
|
+
/** Interactive prompts (TTY only): which pillars, CI, plugin. */
|
|
1212
|
+
async function promptSetup() {
|
|
1213
|
+
const readline = await import("node:readline");
|
|
1214
|
+
const rl = readline.createInterface({
|
|
1215
|
+
input: process.stdin,
|
|
1216
|
+
output: process.stdout,
|
|
1217
|
+
});
|
|
1218
|
+
const ask = (q, def) => new Promise((res) => {
|
|
1219
|
+
rl.question(q, (a) => {
|
|
1220
|
+
res(a.trim() || def);
|
|
1221
|
+
});
|
|
1222
|
+
});
|
|
1223
|
+
const isYes = (s) => /^y(es)?$/i.test(s);
|
|
1224
|
+
try {
|
|
1225
|
+
const pillars = (await ask("Set up which pillars? [both/lint/test] (both): ", "both")).toLowerCase();
|
|
1226
|
+
const gha = isYes(await ask("Wire CI (GitHub Action)? [Y/n]: ", "y"));
|
|
1227
|
+
const plugin = isYes(await ask("Install the Claude Code plugin (hooks + skills)? [Y/n]: ", "y"));
|
|
1228
|
+
return {
|
|
1229
|
+
lint: pillars !== "test",
|
|
1230
|
+
test: pillars !== "lint" && pillars !== "verify",
|
|
1231
|
+
gha,
|
|
1232
|
+
plugin,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
finally {
|
|
1236
|
+
rl.close();
|
|
1045
1237
|
}
|
|
1046
|
-
return false;
|
|
1047
1238
|
}
|
|
1048
1239
|
const KNOWN_INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
1049
1240
|
const KNOWN_OTHER_CONFIGS = {
|
|
@@ -1120,175 +1311,344 @@ function detectProject() {
|
|
|
1120
1311
|
hasClaude: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, ".claude")),
|
|
1121
1312
|
};
|
|
1122
1313
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
const
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
console.log(`Sync tools: ${detected.syncTools.join(", ")}`);
|
|
1138
|
-
}
|
|
1314
|
+
/** The instruction-file targets Pillar 1 will create specs for — the harness's
|
|
1315
|
+
* native instruction file (CLAUDE.md for Claude Code, AGENTS.md for Codex),
|
|
1316
|
+
* plus any existing instruction file that lacks a spec. */
|
|
1317
|
+
function determineTargets(detected, targetValue, harnesses) {
|
|
1318
|
+
if (targetValue)
|
|
1319
|
+
return [targetValue];
|
|
1320
|
+
const targets = [];
|
|
1321
|
+
if (harnesses.includes("claude"))
|
|
1322
|
+
targets.push("CLAUDE.md");
|
|
1323
|
+
if (harnesses.includes("codex"))
|
|
1324
|
+
targets.push("AGENTS.md");
|
|
1325
|
+
if (targets.length === 0)
|
|
1326
|
+
targets.push("CLAUDE.md");
|
|
1327
|
+
// Any existing instruction file without a spec also gets one.
|
|
1139
1328
|
for (const f of detected.instructionFiles) {
|
|
1140
|
-
if (f.
|
|
1141
|
-
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
if (detected.agents.length > 0 ||
|
|
1145
|
-
detected.otherConfigs.length > 0 ||
|
|
1146
|
-
detected.syncTools.length > 0) {
|
|
1147
|
-
console.log("");
|
|
1148
|
-
}
|
|
1149
|
-
// Step 2: Determine targets
|
|
1150
|
-
let targets;
|
|
1151
|
-
if (targetFlag) {
|
|
1152
|
-
targets = [targetFlag.split("=")[1]];
|
|
1329
|
+
if (!f.hasSpec && !targets.includes(f.path))
|
|
1330
|
+
targets.push(f.path);
|
|
1153
1331
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
targets
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1332
|
+
return targets;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* When CLAUDE.md and AGENTS.md are ONE artifact — a symlink, or kept
|
|
1336
|
+
* byte-identical by rulesync/Ruler — collapse them to a single canonical spec
|
|
1337
|
+
* target. Two specs would fight over one file and collide on the integrity hash
|
|
1338
|
+
* (see the "Compose With Sync Tools" rule). The mirror is distributed from the
|
|
1339
|
+
* canonical, not compiled separately.
|
|
1340
|
+
*/
|
|
1341
|
+
function collapseMirroredTargets(targets, mirror) {
|
|
1342
|
+
if (!mirror)
|
|
1343
|
+
return targets;
|
|
1344
|
+
// The compile source slot: the real file for a symlink, else CLAUDE.md (the
|
|
1345
|
+
// one Claude Code reads natively; the sync tool fans out to AGENTS.md).
|
|
1346
|
+
const canonical = mirror.kind === "symlink"
|
|
1347
|
+
? (mirror.realTarget ?? "CLAUDE.md")
|
|
1348
|
+
: "CLAUDE.md";
|
|
1349
|
+
const mirrored = mirror.files.find((f) => f !== canonical);
|
|
1350
|
+
if (!mirrored)
|
|
1351
|
+
return targets;
|
|
1352
|
+
if (!targets.includes(canonical) && !targets.includes(mirrored)) {
|
|
1353
|
+
return targets; // neither file is a target — nothing to collapse
|
|
1354
|
+
}
|
|
1355
|
+
const collapsed = targets.filter((t) => t !== mirrored);
|
|
1356
|
+
if (!collapsed.includes(canonical))
|
|
1357
|
+
collapsed.push(canonical);
|
|
1358
|
+
console.log(`Note: CLAUDE.md and AGENTS.md are one artifact (${mirror.kind}). ` +
|
|
1359
|
+
`Scaffolding a single spec for ${canonical}; ${mirrored} is its mirror ` +
|
|
1360
|
+
`(don't add a second spec — it would collide on the integrity hash).`);
|
|
1361
|
+
return collapsed;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* When a detected rule-sync tool (rulesync / Ruler) regenerates a file vigiles
|
|
1365
|
+
* would compile to, REDIRECT the compile target to the tool's source slot
|
|
1366
|
+
* (`.ruler/AGENTS.md`, `.rulesync/rules/vigiles.md`). vigiles compiles upstream;
|
|
1367
|
+
* the tool distributes to CLAUDE.md/AGENTS.md/Cursor/… — so the integrity hash
|
|
1368
|
+
* never collides with the tool's output (the "Compose With Sync Tools" rule).
|
|
1369
|
+
*/
|
|
1370
|
+
function redirectSyncToolTargets(cwd, targets) {
|
|
1371
|
+
const collisions = (0, compose_js_1.composeCollisions)(cwd, targets);
|
|
1372
|
+
if (collisions.length === 0)
|
|
1373
|
+
return targets;
|
|
1374
|
+
const slotFor = new Map(collisions.map((c) => [c.target, c.redirectTo]));
|
|
1375
|
+
const out = [];
|
|
1376
|
+
for (const t of targets) {
|
|
1377
|
+
const redirected = slotFor.get(t) ?? t;
|
|
1378
|
+
if (!out.includes(redirected))
|
|
1379
|
+
out.push(redirected);
|
|
1380
|
+
}
|
|
1381
|
+
const tool = collisions[0].tool;
|
|
1382
|
+
const slots = [...new Set(collisions.map((c) => c.redirectTo))].join(", ");
|
|
1383
|
+
const from = [...slotFor.keys()].join(", ");
|
|
1384
|
+
console.log(`Note: ${tool} detected — scaffolding the spec to compile into its source ` +
|
|
1385
|
+
`slot (${slots}) instead of ${from}, so ${tool} distributes it without ` +
|
|
1386
|
+
`staling the integrity hash.`);
|
|
1387
|
+
return out;
|
|
1388
|
+
}
|
|
1389
|
+
/** Pillar 1 — specs + types + schema + compile. Scaffolds a spec for every
|
|
1390
|
+
* instruction file (so `--lint` always delivers a spec), but never compiles
|
|
1391
|
+
* OVER a hand-written file — that is left to the migrate-to-spec skill. */
|
|
1392
|
+
async function setupPillar1(detected, targetValue, harnesses) {
|
|
1393
|
+
const cwd = process.cwd();
|
|
1394
|
+
const written = [];
|
|
1395
|
+
const needsMigration = [];
|
|
1396
|
+
// An explicit --target is honoured as-is; otherwise collapse a CLAUDE.md⇄
|
|
1397
|
+
// AGENTS.md mirror (symlink or synced) to one canonical spec, then redirect
|
|
1398
|
+
// into a sync tool's source slot when one would own the output.
|
|
1399
|
+
const targets = targetValue
|
|
1400
|
+
? determineTargets(detected, targetValue, harnesses)
|
|
1401
|
+
: redirectSyncToolTargets(cwd, collapseMirroredTargets(determineTargets(detected, targetValue, harnesses), (0, compose_js_1.detectInstructionMirror)(cwd)));
|
|
1402
|
+
// Create specs (blank). An existing hand-written target keeps its content —
|
|
1403
|
+
// we scaffold the spec but flag it for migration rather than clobbering it.
|
|
1181
1404
|
for (const target of targets) {
|
|
1182
1405
|
const specPath = `${target}.spec.ts`;
|
|
1183
|
-
|
|
1406
|
+
const targetExists = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, target));
|
|
1407
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, specPath))) {
|
|
1184
1408
|
console.log(`✓ ${specPath} already exists`);
|
|
1185
1409
|
}
|
|
1186
|
-
else if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), target))) {
|
|
1187
|
-
console.log(`⚠ ${target} exists without spec — migrate with migrate-to-spec skill`);
|
|
1188
|
-
}
|
|
1189
1410
|
else {
|
|
1190
|
-
init(["--target=" + target]);
|
|
1411
|
+
init(["--target=" + target]); // prints "Created …"
|
|
1412
|
+
written.push(specPath);
|
|
1413
|
+
}
|
|
1414
|
+
if (targetExists && !targetHasHash((0, node_path_1.resolve)(cwd, target))) {
|
|
1415
|
+
needsMigration.push(target);
|
|
1416
|
+
console.log(` ${target} already has content — port it into the spec with the migrate-to-spec skill, then \`vigiles compile\`.`);
|
|
1191
1417
|
}
|
|
1192
1418
|
}
|
|
1193
|
-
//
|
|
1419
|
+
// Generate types + schema.
|
|
1194
1420
|
console.log("\nScanning linters and project files...");
|
|
1195
|
-
const typesResult = (0, generate_types_js_1.generateTypes)({ basePath:
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
if (!(0, node_fs_1.existsSync)(outDir)) {
|
|
1421
|
+
const typesResult = (0, generate_types_js_1.generateTypes)({ basePath: cwd });
|
|
1422
|
+
const outDir = (0, node_path_1.resolve)(cwd, ".vigiles");
|
|
1423
|
+
if (!(0, node_fs_1.existsSync)(outDir))
|
|
1199
1424
|
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
1200
|
-
|
|
1201
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outPath), typesResult.dts);
|
|
1425
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, ".vigiles/generated.d.ts"), typesResult.dts);
|
|
1202
1426
|
for (const l of typesResult.linters) {
|
|
1203
1427
|
console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
|
|
1204
1428
|
}
|
|
1205
1429
|
if (typesResult.scripts.length > 0) {
|
|
1206
1430
|
console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
|
|
1207
1431
|
}
|
|
1208
|
-
console.log(
|
|
1209
|
-
|
|
1210
|
-
// gets rule-name autocomplete + typo squiggles from the editor's YAML LSP.
|
|
1432
|
+
console.log("✓ Generated .vigiles/generated.d.ts");
|
|
1433
|
+
written.push(".vigiles/generated.d.ts");
|
|
1211
1434
|
const schemaResult = (0, generate_schema_js_1.generateSchema)({
|
|
1212
|
-
basePath:
|
|
1435
|
+
basePath: cwd,
|
|
1213
1436
|
linters: (0, validate_js_1.loadConfig)().linters,
|
|
1214
1437
|
});
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
//
|
|
1438
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, ".vigiles/schema.json"), schemaResult.json);
|
|
1439
|
+
console.log("✓ Generated .vigiles/schema.json (YAML-LSP frontmatter schema)");
|
|
1440
|
+
written.push(".vigiles/schema.json");
|
|
1441
|
+
// Compile — but only specs whose target is greenfield or already ours. A
|
|
1442
|
+
// freshly-scaffolded blank spec over an existing hand-written file is skipped
|
|
1443
|
+
// so we never overwrite the user's instructions with an empty compile.
|
|
1219
1444
|
console.log("\nCompiling specs...");
|
|
1220
|
-
const specs = findSpecs()
|
|
1221
|
-
|
|
1445
|
+
const specs = findSpecs().filter((s) => {
|
|
1446
|
+
const tf = (0, node_path_1.resolve)(cwd, s.replace(/\.spec\.ts$/, ""));
|
|
1447
|
+
return !(0, node_fs_1.existsSync)(tf) || targetHasHash(tf);
|
|
1448
|
+
});
|
|
1449
|
+
if (specs.length > 0)
|
|
1222
1450
|
await compile(specs, (0, validate_js_1.loadConfig)());
|
|
1451
|
+
return { specTargets: targets, written, needsMigration };
|
|
1452
|
+
}
|
|
1453
|
+
/** Whether a harness binary (`claude`, `codex`) is on PATH. */
|
|
1454
|
+
function harnessBinaryPresent(bin) {
|
|
1455
|
+
try {
|
|
1456
|
+
const { execSync: exec } = require("node:child_process");
|
|
1457
|
+
exec(`${bin} --version`, { stdio: "ignore", timeout: 10000 });
|
|
1458
|
+
return true;
|
|
1223
1459
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
catch {
|
|
1247
|
-
// skills CLI not available — fall back to direct hook installation
|
|
1248
|
-
}
|
|
1249
|
-
if (!pluginInstalled) {
|
|
1250
|
-
// Fall back: write hooks directly to .claude/settings.json
|
|
1251
|
-
// (gives auto-compile + block edits, but no skills)
|
|
1252
|
-
const settingsDir = (0, node_path_1.resolve)(process.cwd(), ".claude");
|
|
1253
|
-
const settingsPath = (0, node_path_1.resolve)(settingsDir, "settings.json");
|
|
1254
|
-
if (!(0, node_fs_1.existsSync)(settingsDir)) {
|
|
1255
|
-
(0, node_fs_1.mkdirSync)(settingsDir, { recursive: true });
|
|
1256
|
-
}
|
|
1257
|
-
let settings = {};
|
|
1258
|
-
if ((0, node_fs_1.existsSync)(settingsPath)) {
|
|
1259
|
-
try {
|
|
1260
|
-
settings = JSON.parse((0, node_fs_1.readFileSync)(settingsPath, "utf-8"));
|
|
1261
|
-
}
|
|
1262
|
-
catch {
|
|
1263
|
-
// Ignore malformed settings
|
|
1460
|
+
catch {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Install vigiles's skills/hooks for the chosen harness(es) via the per-harness
|
|
1466
|
+
* `planPluginInstall` decision — Claude Code through the GLOBAL plugin
|
|
1467
|
+
* marketplace (nothing vendored into the repo), Codex via AGENTS.md-direct (no
|
|
1468
|
+
* global store). The decision is pure and unit-tested; this is the thin IO.
|
|
1469
|
+
*/
|
|
1470
|
+
function installPlugins(harnesses) {
|
|
1471
|
+
const { execSync: exec } = require("node:child_process");
|
|
1472
|
+
const plans = (0, setup_plan_js_1.planPluginInstall)(harnesses, {
|
|
1473
|
+
hasClaude: harnesses.includes("claude") && harnessBinaryPresent("claude"),
|
|
1474
|
+
});
|
|
1475
|
+
for (const plan of plans) {
|
|
1476
|
+
console.log("");
|
|
1477
|
+
let installed = false;
|
|
1478
|
+
if (plan.commands.length > 0) {
|
|
1479
|
+
try {
|
|
1480
|
+
for (const cmd of plan.commands) {
|
|
1481
|
+
exec(cmd, { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
|
|
1264
1482
|
}
|
|
1483
|
+
console.log(plan.successMessage);
|
|
1484
|
+
installed = true;
|
|
1265
1485
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
}
|
|
1269
|
-
const hooks = settings["hooks"];
|
|
1270
|
-
const preCmd = `FILE=$(cat | jq -r '.tool_input.file_path // empty') && case "$FILE" in *.md) [ -f "$FILE" ] && head -1 "$FILE" | grep -q 'vigiles:sha256:' && { SPEC=$(head -1 "$FILE" | sed -n 's/.*compiled from \\(.*\\) -->/\\1/p'); echo "BLOCKED: Edit $SPEC instead." >&2; exit 2; } ;; esac; exit 0`;
|
|
1271
|
-
const postCmd = `FILE=$(cat | jq -r '.tool_input.file_path // empty') && case "$(basename "$FILE")" in eslint.config.*|.eslintrc*|package.json|pyproject.toml|Cargo.toml) npx vigiles generate-types 2>&1 || true ;; esac && case "$FILE" in *.spec.ts) npx vigiles compile 2>&1 || true ;; esac`;
|
|
1272
|
-
// Append to existing arrays (don't duplicate if already present)
|
|
1273
|
-
const existingStr = JSON.stringify(settings);
|
|
1274
|
-
if (!existingStr.includes("vigiles:sha256")) {
|
|
1275
|
-
const pre = (hooks["PreToolUse"] ?? []);
|
|
1276
|
-
pre.push({ matcher: "Edit|Write", command: preCmd });
|
|
1277
|
-
hooks["PreToolUse"] = pre;
|
|
1278
|
-
}
|
|
1279
|
-
if (!existingStr.includes("vigiles compile")) {
|
|
1280
|
-
const post = (hooks["PostToolUse"] ?? []);
|
|
1281
|
-
post.push({ matcher: "Edit|Write", command: postCmd });
|
|
1282
|
-
hooks["PostToolUse"] = post;
|
|
1486
|
+
catch {
|
|
1487
|
+
// Fall through to the manual instructions below.
|
|
1283
1488
|
}
|
|
1284
|
-
(0, node_fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1285
|
-
console.log("✓ Installed hooks in .claude/settings.json");
|
|
1286
|
-
console.log(" (For skills like edit-spec and migrate-to-spec, also run: npx skills add zernie/vigiles)");
|
|
1287
1489
|
}
|
|
1490
|
+
if (!installed) {
|
|
1491
|
+
console.log(`Install vigiles for ${plan.harness}:`);
|
|
1492
|
+
for (const step of plan.manualSteps)
|
|
1493
|
+
console.log(` ${step}`);
|
|
1494
|
+
}
|
|
1495
|
+
for (const note of plan.notes)
|
|
1496
|
+
console.log(` ${note}`);
|
|
1288
1497
|
}
|
|
1289
|
-
|
|
1498
|
+
}
|
|
1499
|
+
/** Add/upgrade `vigiles` in the project's `devDependencies` (and move it out of
|
|
1500
|
+
* `dependencies` if it's there). Returns the files it wrote (for the commit
|
|
1501
|
+
* hint). No-op in the vigiles repo itself and when there is no package.json. */
|
|
1502
|
+
function ensureVigilesDevDep() {
|
|
1503
|
+
const pkgPath = (0, node_path_1.resolve)(process.cwd(), "package.json");
|
|
1504
|
+
if (!(0, node_fs_1.existsSync)(pkgPath))
|
|
1505
|
+
return [];
|
|
1506
|
+
let pkg;
|
|
1507
|
+
try {
|
|
1508
|
+
pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, "utf-8"));
|
|
1509
|
+
}
|
|
1510
|
+
catch {
|
|
1511
|
+
return [];
|
|
1512
|
+
}
|
|
1513
|
+
if (pkg.name === "vigiles")
|
|
1514
|
+
return []; // don't self-depend in this repo
|
|
1515
|
+
const spec = vigilesDepSpec();
|
|
1516
|
+
let changed = false;
|
|
1517
|
+
// Move a stale/misplaced runtime dependency (e.g. a `github:zernie/vigiles`
|
|
1518
|
+
// git pin, or vigiles sitting in `dependencies`) into devDependencies.
|
|
1519
|
+
if (pkg.dependencies && "vigiles" in pkg.dependencies) {
|
|
1520
|
+
delete pkg.dependencies.vigiles;
|
|
1521
|
+
changed = true;
|
|
1522
|
+
}
|
|
1523
|
+
const dev = (pkg.devDependencies ??= {});
|
|
1524
|
+
if (dev.vigiles !== spec) {
|
|
1525
|
+
dev.vigiles = spec;
|
|
1526
|
+
changed = true;
|
|
1527
|
+
}
|
|
1528
|
+
if (!changed)
|
|
1529
|
+
return [];
|
|
1530
|
+
(0, node_fs_1.writeFileSync)(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1531
|
+
console.log(`✓ Set vigiles@${spec} in devDependencies — run \`npm install\` to fetch it`);
|
|
1532
|
+
return ["package.json"];
|
|
1533
|
+
}
|
|
1534
|
+
/** Which harnesses to set up: an explicit `--harness=` list, else auto-detected
|
|
1535
|
+
* from the repo (Claude Code / Codex), defaulting to Claude Code. */
|
|
1536
|
+
function resolveHarnesses(parsed, detected) {
|
|
1537
|
+
if (parsed.harness) {
|
|
1538
|
+
return parsed.harness
|
|
1539
|
+
.split(",")
|
|
1540
|
+
.map((s) => s.trim().toLowerCase())
|
|
1541
|
+
.filter(Boolean);
|
|
1542
|
+
}
|
|
1543
|
+
const set = new Set();
|
|
1544
|
+
if (detected.hasClaude ||
|
|
1545
|
+
detected.agents.includes("Claude Code") ||
|
|
1546
|
+
detected.instructionFiles.some((f) => f.path === "CLAUDE.md")) {
|
|
1547
|
+
set.add("claude");
|
|
1548
|
+
}
|
|
1549
|
+
if (detected.agents.includes("Codex / GitHub Copilot") ||
|
|
1550
|
+
detected.instructionFiles.some((f) => f.path === "AGENTS.md")) {
|
|
1551
|
+
set.add("codex");
|
|
1552
|
+
}
|
|
1553
|
+
if (set.size === 0)
|
|
1554
|
+
set.add("claude");
|
|
1555
|
+
return [...set];
|
|
1556
|
+
}
|
|
1557
|
+
/** Print the project-detection summary line(s). */
|
|
1558
|
+
function printDetection(detected, harnesses) {
|
|
1559
|
+
if (detected.agents.length > 0) {
|
|
1560
|
+
console.log(`Detected: ${detected.agents.join(", ")}`);
|
|
1561
|
+
}
|
|
1562
|
+
if (detected.otherConfigs.length > 0) {
|
|
1563
|
+
console.log(`Other agent configs: ${detected.otherConfigs.join(", ")}`);
|
|
1564
|
+
}
|
|
1565
|
+
if (detected.syncTools.length > 0) {
|
|
1566
|
+
console.log(`Sync tools: ${detected.syncTools.join(", ")}`);
|
|
1567
|
+
}
|
|
1568
|
+
for (const f of detected.instructionFiles) {
|
|
1569
|
+
if (f.isSymlink)
|
|
1570
|
+
console.log(`Note: ${f.path} is a symlink`);
|
|
1571
|
+
}
|
|
1572
|
+
console.log(`Harness: ${harnesses.join(", ")}`);
|
|
1573
|
+
}
|
|
1574
|
+
/** Print the closing next-steps list + an honest commit hint (only files
|
|
1575
|
+
* actually written this run). */
|
|
1576
|
+
function printSetupSummary(opts) {
|
|
1577
|
+
const { plan, strict, targets, needsMigration, written } = opts;
|
|
1290
1578
|
const specPathsList = targets.map((t) => `${t}.spec.ts`);
|
|
1291
|
-
|
|
1579
|
+
console.log("\n---\nSetup complete.\n");
|
|
1580
|
+
const nextSteps = [];
|
|
1581
|
+
if (needsMigration.length > 0) {
|
|
1582
|
+
nextSteps.push(`Port ${needsMigration.join(", ")} into its spec with the migrate-to-spec skill, then \`npx vigiles compile\``);
|
|
1583
|
+
}
|
|
1584
|
+
else if (specPathsList.length > 0) {
|
|
1585
|
+
nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen\``);
|
|
1586
|
+
}
|
|
1587
|
+
if (plan.test) {
|
|
1588
|
+
nextSteps.push("Edit vigiles.harness.mjs to test a real hook, then `npx vigiles test`");
|
|
1589
|
+
}
|
|
1590
|
+
if (written.includes("package.json")) {
|
|
1591
|
+
nextSteps.push("Run `npm install` to fetch the vigiles dev dependency");
|
|
1592
|
+
}
|
|
1593
|
+
if (!strict) {
|
|
1594
|
+
nextSteps.push("When ready, enforce in CI: npx vigiles init --strict");
|
|
1595
|
+
}
|
|
1596
|
+
nextSteps.forEach((s, i) => {
|
|
1597
|
+
console.log(` ${String(i + 1)}. ${s}`);
|
|
1598
|
+
});
|
|
1599
|
+
// Only list files actually written this run (deduped, in a stable order).
|
|
1600
|
+
const files = [...new Set(written)];
|
|
1601
|
+
if (files.length > 0) {
|
|
1602
|
+
console.log(`\n Commit:\n git add ${files.join(" ")} && git commit -m "Add vigiles"`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
async function setup(args) {
|
|
1606
|
+
const parsed = (0, setup_plan_js_1.parseSetupArgs)(args);
|
|
1607
|
+
const strict = parsed.strict;
|
|
1608
|
+
// Plan: defaults → flags → interactive prompts (only a human at a TTY).
|
|
1609
|
+
let plan = (0, setup_plan_js_1.resolvePlan)(parsed);
|
|
1610
|
+
if ((0, setup_plan_js_1.shouldPrompt)(parsed, process.stdin.isTTY ?? false)) {
|
|
1611
|
+
plan = (0, setup_plan_js_1.resolvePlan)(parsed, await promptSetup());
|
|
1612
|
+
}
|
|
1613
|
+
const pillars = [plan.lint && "lint", plan.test && "test"]
|
|
1614
|
+
.filter(Boolean)
|
|
1615
|
+
.join(" + ");
|
|
1616
|
+
console.log(`vigiles setup${strict ? " (strict)" : ""} — pillars: ${pillars}\n`);
|
|
1617
|
+
// Detect project.
|
|
1618
|
+
const detected = detectProject();
|
|
1619
|
+
const harnesses = resolveHarnesses(parsed, detected);
|
|
1620
|
+
printDetection(detected, harnesses);
|
|
1621
|
+
// Files actually written, accumulated for an honest commit hint.
|
|
1622
|
+
const written = [];
|
|
1623
|
+
// Lint pillar — verify instruction-file references.
|
|
1624
|
+
let targets = [];
|
|
1625
|
+
let needsMigration = [];
|
|
1626
|
+
if (plan.lint) {
|
|
1627
|
+
console.log("");
|
|
1628
|
+
const p1 = await setupPillar1(detected, parsed.target, harnesses);
|
|
1629
|
+
targets = p1.specTargets;
|
|
1630
|
+
needsMigration = p1.needsMigration;
|
|
1631
|
+
written.push(...p1.written);
|
|
1632
|
+
}
|
|
1633
|
+
// Test pillar — test the harness.
|
|
1634
|
+
if (plan.test) {
|
|
1635
|
+
console.log("");
|
|
1636
|
+
written.push(...scaffoldPillar2());
|
|
1637
|
+
}
|
|
1638
|
+
// Add/upgrade the vigiles dev dependency (both pillars import from it).
|
|
1639
|
+
if (plan.lint || plan.test) {
|
|
1640
|
+
written.push(...ensureVigilesDevDep());
|
|
1641
|
+
}
|
|
1642
|
+
// CI — the production Action (+ a harness job when Pillar 2 is on).
|
|
1643
|
+
if (plan.gha) {
|
|
1644
|
+
console.log("");
|
|
1645
|
+
written.push(...wireGha(plan));
|
|
1646
|
+
}
|
|
1647
|
+
// Plugin/skill install — per-harness (Claude marketplace / Codex direct).
|
|
1648
|
+
if (plan.plugin) {
|
|
1649
|
+
installPlugins(harnesses);
|
|
1650
|
+
}
|
|
1651
|
+
// Agent-specific guidance.
|
|
1292
1652
|
if (targets.includes("AGENTS.md")) {
|
|
1293
1653
|
console.log("\n Codex / Copilot reads AGENTS.md directly — no hooks needed.");
|
|
1294
1654
|
console.log(" Run `npx vigiles compile` after spec edits. CI enforces freshness.");
|
|
@@ -1297,37 +1657,16 @@ async function setup(args) {
|
|
|
1297
1657
|
console.log("\n Non-markdown agent configs detected. Use a sync tool to convert:");
|
|
1298
1658
|
console.log(" npm install -D rule-porter");
|
|
1299
1659
|
}
|
|
1300
|
-
//
|
|
1660
|
+
// Strict config.
|
|
1301
1661
|
if (strict) {
|
|
1302
1662
|
const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
|
|
1303
1663
|
if (!(0, node_fs_1.existsSync)(configPath)) {
|
|
1304
|
-
(0, node_fs_1.writeFileSync)(configPath, JSON.stringify({
|
|
1305
|
-
rules: {
|
|
1306
|
-
"require-spec": "error",
|
|
1307
|
-
"require-skill-spec": "error",
|
|
1308
|
-
},
|
|
1309
|
-
}, null, 2) + "\n");
|
|
1664
|
+
(0, node_fs_1.writeFileSync)(configPath, JSON.stringify({ rules: { "require-spec": "error", "require-skill-spec": "error" } }, null, 2) + "\n");
|
|
1310
1665
|
console.log("✓ Created .vigilesrc.json with strict rules");
|
|
1666
|
+
written.push(".vigilesrc.json");
|
|
1311
1667
|
}
|
|
1312
1668
|
}
|
|
1313
|
-
|
|
1314
|
-
console.log("\n---");
|
|
1315
|
-
console.log("Setup complete.\n");
|
|
1316
|
-
console.log(` 1. Edit ${specPaths} — add your project's conventions`);
|
|
1317
|
-
console.log(" 2. Run `/strengthen` in Claude Code to upgrade guidance → enforce");
|
|
1318
|
-
if (!strict) {
|
|
1319
|
-
console.log(" 3. When ready, enforce specs in CI: npx vigiles setup --strict");
|
|
1320
|
-
}
|
|
1321
|
-
console.log("\n Commit:");
|
|
1322
|
-
const files = [
|
|
1323
|
-
...targets,
|
|
1324
|
-
...specPathsList,
|
|
1325
|
-
".vigiles/generated.d.ts",
|
|
1326
|
-
".vigiles/schema.json",
|
|
1327
|
-
...(shouldInstallPlugin ? [".claude/settings.json"] : []),
|
|
1328
|
-
...(strict ? [".vigilesrc.json"] : []),
|
|
1329
|
-
];
|
|
1330
|
-
console.log(` git add ${files.join(" ")} && git commit -m "Add vigiles spec"`);
|
|
1669
|
+
printSetupSummary({ plan, strict, targets, needsMigration, written });
|
|
1331
1670
|
}
|
|
1332
1671
|
// ---------------------------------------------------------------------------
|
|
1333
1672
|
// Strengthen: guidance() → enforce() suggestions
|
|
@@ -1358,6 +1697,32 @@ function checkIntegrityForFiles(files, severity, silent) {
|
|
|
1358
1697
|
}
|
|
1359
1698
|
return severity === "error" ? errorCount : 0;
|
|
1360
1699
|
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Apply the `untested-surface` rule: find skills/agents/hooks with no test or
|
|
1702
|
+
* eval (see src/test-coverage.ts). Returns the raw untested count plus the
|
|
1703
|
+
* severity-gated error count — "warn" prints but never fails CI (errors=0),
|
|
1704
|
+
* "error" fails (exit 2), mirroring the integrity check.
|
|
1705
|
+
*/
|
|
1706
|
+
function checkUntestedSurfaces(config, silent) {
|
|
1707
|
+
const severity = (0, types_js_1.ruleSeverity)(config?.rules["untested-surface"]);
|
|
1708
|
+
if (!severity)
|
|
1709
|
+
return { untested: 0, errors: 0 };
|
|
1710
|
+
const opts = (0, types_js_1.ruleOptions)(config?.rules["untested-surface"]);
|
|
1711
|
+
const report = (0, test_coverage_js_1.findUntestedSurfaces)({ basePath: process.cwd(), ...opts });
|
|
1712
|
+
if (!silent) {
|
|
1713
|
+
console.log("\nUntested surfaces:\n");
|
|
1714
|
+
for (const line of (0, test_coverage_js_1.formatUntestedReport)(report).split("\n")) {
|
|
1715
|
+
console.log(` ${line}`);
|
|
1716
|
+
}
|
|
1717
|
+
for (const s of report.untested) {
|
|
1718
|
+
ghAnnotate(severity === "error" ? "error" : "warning", `${s.kind} ${s.path} ships without a test or eval`, s.path);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return {
|
|
1722
|
+
untested: report.untested.length,
|
|
1723
|
+
errors: severity === "error" ? report.untested.length : 0,
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1361
1726
|
/**
|
|
1362
1727
|
* Apply the configured coverage thresholds. Returns the number of failing
|
|
1363
1728
|
* thresholds (so the audit can fail CI when severity is "error").
|
|
@@ -1534,16 +1899,19 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
1534
1899
|
*/
|
|
1535
1900
|
function handleRunScripts(kind, args, restArgs) {
|
|
1536
1901
|
const cwd = process.cwd();
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
console.log("vigiles test: `claude` CLI not found — skipping harness tests.");
|
|
1540
|
-
return;
|
|
1541
|
-
}
|
|
1902
|
+
// Harness/eval scripts may be authored in JS or TS (see run-scripts.ts).
|
|
1903
|
+
const defaultGlob = (0, run_scripts_js_1.scriptGlob)(kind === "test" ? "harness" : "eval");
|
|
1542
1904
|
const files = (0, run_scripts_js_1.discoverScripts)(restArgs, defaultGlob, cwd);
|
|
1543
1905
|
if (files.length === 0) {
|
|
1544
1906
|
console.log(`No ${defaultGlob} files found.`);
|
|
1545
1907
|
return;
|
|
1546
1908
|
}
|
|
1909
|
+
// No blanket skip: unit-tier (runHook) tests need no `claude`, so always run.
|
|
1910
|
+
// A script whose tier DOES need `claude` self-reports `⊘ SKIPPED` (exit 77) —
|
|
1911
|
+
// loud, never a silent green. Just flag up front that some may skip.
|
|
1912
|
+
if (kind === "test" && !(0, harness_test_js_1.claudeAvailable)()) {
|
|
1913
|
+
console.log("ℹ `claude` CLI not found — unit-tier tests run; tests that need it report SKIPPED.\n");
|
|
1914
|
+
}
|
|
1547
1915
|
const trialsFlag = args.find((a) => a.startsWith("--trials="));
|
|
1548
1916
|
const env = {};
|
|
1549
1917
|
if (trialsFlag)
|
|
@@ -1551,16 +1919,24 @@ function handleRunScripts(kind, args, restArgs) {
|
|
|
1551
1919
|
console.log(`Running ${String(files.length)} ${kind} file(s):\n`);
|
|
1552
1920
|
const results = (0, run_scripts_js_1.runScripts)(files, cwd, env);
|
|
1553
1921
|
console.log("\n" + (0, run_scripts_js_1.formatScriptSummary)(results));
|
|
1554
|
-
if (
|
|
1922
|
+
if ((0, run_scripts_js_1.anyFailed)(results))
|
|
1923
|
+
process.exit(1);
|
|
1924
|
+
// `--no-skip`: in a context that ASSERTS the capability is present (a CI job),
|
|
1925
|
+
// a skipped tier is untested surface — fail loudly instead of passing green.
|
|
1926
|
+
if (args.includes("--no-skip") && results.some((r) => r.status === "skip")) {
|
|
1927
|
+
const n = results.filter((r) => r.status === "skip").length;
|
|
1928
|
+
console.log(`\n✗ --no-skip: ${String(n)} tier(s) SKIPPED — untested surface here. ` +
|
|
1929
|
+
"Install the missing capability (e.g. the `claude` CLI) or scope the run.");
|
|
1555
1930
|
process.exit(1);
|
|
1931
|
+
}
|
|
1556
1932
|
}
|
|
1557
1933
|
function printUsage(command) {
|
|
1558
1934
|
console.log("vigiles — compile typed specs to instruction files");
|
|
1559
1935
|
console.log("");
|
|
1560
1936
|
console.log("Commands:");
|
|
1561
|
-
console.log(" vigiles init [flags] Setup project (--
|
|
1937
|
+
console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --no-gha)");
|
|
1562
1938
|
console.log(" vigiles compile [files...] Compile .spec.ts → .md");
|
|
1563
|
-
console.log(" vigiles
|
|
1939
|
+
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
1564
1940
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
1565
1941
|
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N)");
|
|
1566
1942
|
console.log("");
|
|
@@ -1574,6 +1950,7 @@ function printUsage(command) {
|
|
|
1574
1950
|
console.log(" vigiles generate-types --check Verify .d.ts is up to date");
|
|
1575
1951
|
console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
|
|
1576
1952
|
console.log(" vigiles generate-schema --check Verify schema.json is up to date");
|
|
1953
|
+
console.log(" vigiles --version Print the version number");
|
|
1577
1954
|
if (command && command !== "--help") {
|
|
1578
1955
|
console.log(`\nUnknown command: "${command}"`);
|
|
1579
1956
|
process.exit(1);
|
|
@@ -1785,17 +2162,10 @@ function isInstructionFile(file) {
|
|
|
1785
2162
|
* was found. `basePath` is the file's own directory (where paths resolve).
|
|
1786
2163
|
*/
|
|
1787
2164
|
function reportRefIssues(markdown, basePath, log) {
|
|
1788
|
-
const
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
}
|
|
1793
|
-
for (const u of unmarked) {
|
|
1794
|
-
const callee = u.text.replace(/\s*\([^)]*\)\s*$/, "");
|
|
1795
|
-
log(` ✗ line ${String(u.line)}: \`${u.text}\` is an unmarked code reference — ` +
|
|
1796
|
-
`mark it as \`vigiles:symbol path/to/file.ext#${callee}\` or add <!-- vigiles:ignore --> if it is prose`);
|
|
1797
|
-
}
|
|
1798
|
-
return broken.length > 0 || unmarked.length > 0;
|
|
2165
|
+
const issues = (0, refs_js_1.collectRefIssues)(markdown, basePath);
|
|
2166
|
+
for (const m of issues)
|
|
2167
|
+
log(` ✗ ${m}`);
|
|
2168
|
+
return issues.length > 0;
|
|
1799
2169
|
}
|
|
1800
2170
|
/** `vigiles refs <file>` — check a file's symbol references (exit 2 on issues). */
|
|
1801
2171
|
function refsCommand(target) {
|
|
@@ -1845,6 +2215,9 @@ function refsHookCommand() {
|
|
|
1845
2215
|
}
|
|
1846
2216
|
if (!file || !isInstructionFile(file))
|
|
1847
2217
|
return;
|
|
2218
|
+
const severity = (0, types_js_1.ruleSeverity)((0, validate_js_1.loadConfig)().rules["unmarked-refs"]);
|
|
2219
|
+
if (severity === false)
|
|
2220
|
+
return;
|
|
1848
2221
|
const cwd = process.cwd();
|
|
1849
2222
|
const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
|
|
1850
2223
|
let markdown;
|
|
@@ -1854,22 +2227,45 @@ function refsHookCommand() {
|
|
|
1854
2227
|
catch {
|
|
1855
2228
|
return;
|
|
1856
2229
|
}
|
|
1857
|
-
const
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
if (
|
|
2230
|
+
const issues = (0, refs_js_1.collectRefIssues)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file)));
|
|
2231
|
+
const action = (0, refs_js_1.refsHookAction)(issues.length, severity);
|
|
2232
|
+
if (action === "ok")
|
|
2233
|
+
return;
|
|
2234
|
+
if (action === "block") {
|
|
2235
|
+
// Opt-in (`unmarked-refs: "error"`): exit 2 feeds stderr to the model.
|
|
1862
2236
|
console.error(`vigiles: fix the code references in ${target}:`);
|
|
1863
|
-
for (const
|
|
1864
|
-
console.error(
|
|
2237
|
+
for (const m of issues)
|
|
2238
|
+
console.error(` ✗ ${m}`);
|
|
1865
2239
|
process.exit(2);
|
|
1866
2240
|
}
|
|
2241
|
+
// Default ("warn"): a non-blocking nudge injected into the agent's context.
|
|
2242
|
+
const context = `vigiles: ${target} has reference(s) that won't be verified unless they ` +
|
|
2243
|
+
`are vigiles marks:\n` +
|
|
2244
|
+
issues.map((m) => ` - ${m}`).join("\n") +
|
|
2245
|
+
`\nExpress references as marks (\`enforce()\` / \`file()\` / \`cmd()\` / a ` +
|
|
2246
|
+
`\`vigiles:symbol\` span / an inline \`<!-- vigiles:enforce -->\` comment) so ` +
|
|
2247
|
+
`\`vigiles audit\` can check them — or add \`<!-- vigiles:ignore -->\` if it ` +
|
|
2248
|
+
`is prose, not a reference.`;
|
|
2249
|
+
process.stdout.write(JSON.stringify({
|
|
2250
|
+
hookSpecificOutput: {
|
|
2251
|
+
hookEventName: "PostToolUse",
|
|
2252
|
+
additionalContext: context,
|
|
2253
|
+
},
|
|
2254
|
+
}) + "\n");
|
|
1867
2255
|
}
|
|
1868
2256
|
async function main() {
|
|
1869
2257
|
const args = process.argv.slice(2);
|
|
1870
2258
|
const command = args[0];
|
|
2259
|
+
// `--version` / `-v` / `version` prints the version number, not the help
|
|
2260
|
+
// banner — so `npx vigiles --version` reports e.g. `3.0.0`.
|
|
2261
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
2262
|
+
console.log(getVersion());
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
1871
2265
|
const restArgs = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
1872
|
-
|
|
2266
|
+
// Shared flags (--max-rules, --catalog-only) override the loaded config so
|
|
2267
|
+
// every GitHub Action input maps to a real CLI flag. See src/cli-flags.ts.
|
|
2268
|
+
const config = (0, cli_flags_js_1.applyConfigFlags)((0, validate_js_1.loadConfig)(), args);
|
|
1873
2269
|
switch (command) {
|
|
1874
2270
|
// --- Primary commands ---
|
|
1875
2271
|
case "init": {
|
|
@@ -1904,8 +2300,8 @@ async function main() {
|
|
|
1904
2300
|
}
|
|
1905
2301
|
break;
|
|
1906
2302
|
}
|
|
1907
|
-
case "
|
|
1908
|
-
//
|
|
2303
|
+
case "lint": {
|
|
2304
|
+
// lint = verify references + discover + guidance count
|
|
1909
2305
|
const flags = args.slice(1).filter((a) => a.startsWith("--"));
|
|
1910
2306
|
const report = await audit(restArgs, flags, config);
|
|
1911
2307
|
annotateAuditForGitHub(report, flags);
|
|
@@ -1921,6 +2317,35 @@ async function main() {
|
|
|
1921
2317
|
case "eval":
|
|
1922
2318
|
handleRunScripts("eval", args, restArgs);
|
|
1923
2319
|
break;
|
|
2320
|
+
case "scan": {
|
|
2321
|
+
const dirs = restArgs.length > 0 ? restArgs : ["."];
|
|
2322
|
+
const json = args.includes("--json");
|
|
2323
|
+
if (dirs.length > 1) {
|
|
2324
|
+
// Multiple targets → rank them (the leaderboard engine).
|
|
2325
|
+
const scores = (0, leaderboard_js_1.rankPlugins)(dirs);
|
|
2326
|
+
console.log(json ? JSON.stringify(scores, null, 2) : (0, leaderboard_js_1.formatLeaderboard)(scores));
|
|
2327
|
+
}
|
|
2328
|
+
else {
|
|
2329
|
+
const root = (0, node_path_1.resolve)(dirs[0]);
|
|
2330
|
+
const harnessFlag = args
|
|
2331
|
+
.find((a) => a.startsWith("--harness="))
|
|
2332
|
+
?.slice("--harness=".length);
|
|
2333
|
+
const det = (0, adapter_registry_js_1.detectAdapterResult)(root);
|
|
2334
|
+
const adapter = harnessFlag
|
|
2335
|
+
? (0, adapter_registry_js_1.resolveAdapter)(root, harnessFlag)
|
|
2336
|
+
: det.adapter;
|
|
2337
|
+
const report = (0, scan_js_1.scanPlugin)(dirs[0], adapter.layout);
|
|
2338
|
+
if (!json) {
|
|
2339
|
+
console.log(`Detected harness: ${adapter.name}`);
|
|
2340
|
+
if (!harnessFlag && det.ambiguousWith.length > 0) {
|
|
2341
|
+
console.log(`⚠ repo also matches: ${det.ambiguousWith.join(", ")} — override with --harness=<name>`);
|
|
2342
|
+
}
|
|
2343
|
+
console.log("");
|
|
2344
|
+
}
|
|
2345
|
+
console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
|
|
2346
|
+
}
|
|
2347
|
+
break;
|
|
2348
|
+
}
|
|
1924
2349
|
// --- Plumbing ---
|
|
1925
2350
|
case "generate-types":
|
|
1926
2351
|
handleGenerateTypes(args, restArgs);
|