@christang/keel 5.6.0 → 5.14.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 +41 -0
- package/assets/bootstrap/AGENTS.md +1 -1
- package/assets/openspec/schemas/keel-spec-driven/templates/tasks.md +4 -2
- package/bin/keel.js +219 -59
- package/package.json +1 -1
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/plugins/keel/agents/keel-single-task-goal-claude.md +3 -2
- package/plugins/keel/agents/keel-single-task-goal-codex.md +2 -2
- package/plugins/keel/scripts/pretooluse-guard.js +65 -2
- package/plugins/keel/scripts/session-start.js +87 -0
- package/plugins/keel/skills/keel-align-expectations/SKILL.md +21 -0
- package/plugins/keel/skills/keel-review-checklist/SKILL.md +25 -1
- package/plugins/keel/skills/keel-run-single-task-goal/SKILL.md +3 -3
- package/scripts/run_python.js +101 -5
- package/scripts/validate_plugin.py +7762 -3848
- package/src/core/config.js +128 -8
- package/src/core/context.js +41 -5
- package/src/core/gates.js +236 -35
- package/src/core/goal.js +13 -1
- package/src/core/helper.js +27 -1
- package/src/core/projection.js +55 -0
- package/src/core/task-contract.js +30 -0
|
@@ -108,6 +108,88 @@ function precedentPointer(cwd) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// The remedy is the host's, so it is named rather than run. Only Claude's
|
|
112
|
+
// command is stated outright, because that is the manifest whose host command
|
|
113
|
+
// Keel has verified; an unprobed target gets a description instead of an
|
|
114
|
+
// invented command line.
|
|
115
|
+
const HOST_UPDATE = "your host's own plugin update command";
|
|
116
|
+
|
|
117
|
+
// The plugin's own manifest sits beside this script, so its version is read
|
|
118
|
+
// from `__dirname` rather than from CLAUDE_PLUGIN_ROOT: the path this file was
|
|
119
|
+
// loaded from is a fact, while an environment variable is a claim the host may
|
|
120
|
+
// not have made. Whichever target's manifest is present is the one that ran.
|
|
121
|
+
function pluginManifest() {
|
|
122
|
+
const targets = [
|
|
123
|
+
[".claude-plugin", "`claude plugin update`"],
|
|
124
|
+
[".codex-plugin", HOST_UPDATE],
|
|
125
|
+
];
|
|
126
|
+
for (const [dir, remedy] of targets) {
|
|
127
|
+
try {
|
|
128
|
+
const manifest = path.join(__dirname, "..", dir, "plugin.json");
|
|
129
|
+
const value = JSON.parse(fs.readFileSync(manifest, "utf8")).version;
|
|
130
|
+
if (typeof value === "string" && value.trim()) {
|
|
131
|
+
return { version: value.trim(), remedy };
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// A missing or unreadable manifest is undiscoverable, not drift.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { version: null, remedy: HOST_UPDATE };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The repository states which protocol it runs in the managed block the
|
|
141
|
+
// installer wrote. AGENTS.md is the canonical carrier; CLAUDE.md is read second
|
|
142
|
+
// because a repository may carry only the target-native file.
|
|
143
|
+
function protocolVersion(cwd) {
|
|
144
|
+
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
145
|
+
try {
|
|
146
|
+
const text = fs.readFileSync(path.join(cwd, name), "utf8");
|
|
147
|
+
const match = text.match(
|
|
148
|
+
/<!--\s*keel:start\s+version=(\d+\.\d+\.\d+)\s*-->/
|
|
149
|
+
);
|
|
150
|
+
if (match) return match[1];
|
|
151
|
+
} catch {
|
|
152
|
+
// Same as above: absent is not mismatched.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Three versions are comparable in any repository: the plugin executing this
|
|
159
|
+
// hook, the CLI it just invoked, and the protocol version the repository
|
|
160
|
+
// stamped into its managed block. Keel reports the disagreement and stops
|
|
161
|
+
// there — installing and updating are the host's, which already has commands
|
|
162
|
+
// for both.
|
|
163
|
+
function versionReport(cwd, cli) {
|
|
164
|
+
const plugin = pluginManifest();
|
|
165
|
+
const found = [
|
|
166
|
+
["plugin", plugin.version],
|
|
167
|
+
["CLI", cli],
|
|
168
|
+
["protocol", protocolVersion(cwd)],
|
|
169
|
+
];
|
|
170
|
+
// Missing is not mismatched. A version nobody can discover never produces a
|
|
171
|
+
// line on its own, or a repository with no managed block would be warned at
|
|
172
|
+
// every session until its reader stopped looking — and fewer than two
|
|
173
|
+
// readable versions is not agreement either, it is nothing to compare.
|
|
174
|
+
const known = found.filter(([, value]) => value);
|
|
175
|
+
if (known.length < 2) return null;
|
|
176
|
+
// Silence when they agree. This line exists to be noticed, and one printed
|
|
177
|
+
// every session stops being read long before the session it mattered in.
|
|
178
|
+
if (new Set(known.map(([, value]) => value)).size === 1) return null;
|
|
179
|
+
const named = known.map(([name, value]) => `${name} ${value}`).join(", ");
|
|
180
|
+
// Naming what was never read is what keeps a partial comparison from being
|
|
181
|
+
// read as a complete one.
|
|
182
|
+
const unread = found
|
|
183
|
+
.filter(([, value]) => !value)
|
|
184
|
+
.map(([name]) => name)
|
|
185
|
+
.join(" and ");
|
|
186
|
+
const missing = unread ? ` (${unread} undiscovered, not compared)` : "";
|
|
187
|
+
return `runtime versions disagree: ${named}${missing}. A session's hooks are `
|
|
188
|
+
+ "fixed at session start, so an updated plugin applies only after "
|
|
189
|
+
+ `restarting. Updating is ${plugin.remedy}, which Keel names and does `
|
|
190
|
+
+ "not run.";
|
|
191
|
+
}
|
|
192
|
+
|
|
111
193
|
// The Keel mark. A keel is the carina, the ridge on a bird's sternum, so the
|
|
112
194
|
// animal that literally has one is a bird. Every cell is drawn from
|
|
113
195
|
// U+2580–U+259F — the same block-element family as the host's own startup
|
|
@@ -293,6 +375,11 @@ function main() {
|
|
|
293
375
|
+ "does not guess among candidates."
|
|
294
376
|
);
|
|
295
377
|
}
|
|
378
|
+
const drift = versionReport(cwd, versionMatch[0]);
|
|
379
|
+
if (drift) {
|
|
380
|
+
lines.push(`- ${drift}`);
|
|
381
|
+
human.splice(human.length - 1, 0, drift[0].toUpperCase() + drift.slice(1));
|
|
382
|
+
}
|
|
296
383
|
const pointer = precedentPointer(cwd);
|
|
297
384
|
if (pointer) lines.push(`- ${pointer}`);
|
|
298
385
|
lines.push(`- report this state ${DISCLOSURE}; it authorizes nothing.`);
|
|
@@ -71,6 +71,27 @@ Three rules govern use:
|
|
|
71
71
|
A precedent informs a decision and never substitutes for a proof: gates, evidence, Review, and the
|
|
72
72
|
write guard are untouched by anything in the store.
|
|
73
73
|
|
|
74
|
+
## Unattended runs
|
|
75
|
+
|
|
76
|
+
Work enters an unattended run only by the repository's declared triage policy — an issue carrying a
|
|
77
|
+
label listed under `triage:` in `keel/config.yaml`, evaluated with `keel triage --labels <labels>`.
|
|
78
|
+
Pass what `gh` returned; Keel never fetches the issue. Admission comes from that declaration and
|
|
79
|
+
never from a precedent, however much triage history the store accumulates: whether an issue becomes
|
|
80
|
+
work is a materiality decision, and a precedent may not move one out of that list.
|
|
81
|
+
|
|
82
|
+
Admission answers "may this begin" and decides nothing after it. Alignment still escalates material
|
|
83
|
+
choices, the gates still run, and the write guard still binds.
|
|
84
|
+
|
|
85
|
+
An unattended run may triage, author, implement, verify, push where `authorize:` permits, and
|
|
86
|
+
**open a pull request**. It **may not merge** one — merging is where an unreviewed decision becomes
|
|
87
|
+
the project's history, and no declaration in Keel authorizes it.
|
|
88
|
+
|
|
89
|
+
Stopping at a decision the user must make is the **designed boundary rather than a failure**.
|
|
90
|
+
Report where the run stopped and why. Do not widen the triage policy to stop it happening.
|
|
91
|
+
|
|
92
|
+
**Keel schedules nothing.** `/loop`, cron, and CI triggers belong to the host runtime; Keel's part
|
|
93
|
+
is making each step decidable with authority.
|
|
94
|
+
|
|
74
95
|
## Domain lenses
|
|
75
96
|
|
|
76
97
|
When the change signals a specific domain, look in `keel/lenses/` for a lens whose `Applies when:` header matches, and read only that lens before asking domain questions; do not load unrelated lenses. When no lens matches, or the repo defines none, proceed on the domain-agnostic path. Lenses are user-authored; scaffold the bundled starting points with `keel lenses add` (web, hardware, hardware-dsl).
|
|
@@ -21,6 +21,8 @@ Read the selected OpenSpec proposal, design, specs, tasks, diff, and command evi
|
|
|
21
21
|
- Confirm the task's Evidence `Contract` line records the task-start capsule fingerprint and that completion recompiled the same fingerprint; a drift result returns the task to authoring for explicit reauthorization instead of review.
|
|
22
22
|
- A behavioral task's checks must prove observable Acceptance through the public interface, not build-only or shape-only evidence.
|
|
23
23
|
- For a red-green strategy (`vertical-tdd`, `regression-first`), confirm concrete per-label `.red` and `.green` Evidence exists for the same check; evidence-first tasks instead name their observable proof.
|
|
24
|
+
- A failure message must **name the actual cause** of what it reports. Watch for one condition guarding **two distinct failures** — `if result is None or result["status"] != expected` reports the first failure's message when the second one happened, sending the reader to a place with no problem in it. Split the condition. No gate can judge this: deciding whether a sentence misleads needs a model, so it stays here.
|
|
25
|
+
- When two tasks in the change declared the same Touch set under a red-green strategy — `keel gate task-start` warns about this — ask whether they turned out to be **one behavior** split in half. The tell is that the first task's minimal implementation was wrong in the field, or that the second had no honest red left because the first already made its checks pass. The gate can only see the shape; by completion you can see the outcome, which is the only point at which this is answerable.
|
|
24
26
|
- When no trustworthy explicit Git base exists, do not attribute dirty paths automatically. Review scope semantically.
|
|
25
27
|
- For `Coupling: required`, confirm one complete candidate reached its completion gate and generated artifacts are aligned.
|
|
26
28
|
|
|
@@ -31,7 +33,22 @@ Record the current agent's judgment inside the selected task Evidence:
|
|
|
31
33
|
- `Status`: `pass` only when the task is ready to complete.
|
|
32
34
|
- `Acceptance check`: why the behavior evidence proves the authored Acceptance.
|
|
33
35
|
- `Scope check`: why the actual changes stay within Touch; identify an explicit base if deterministic comparison was used.
|
|
34
|
-
- `Findings`: `none`, or
|
|
36
|
+
- `Findings`: `none`, or every finding with the disposition it actually has.
|
|
37
|
+
|
|
38
|
+
A finding has three, and the criterion is what the task did about it — not
|
|
39
|
+
whichever marker gets past the gate:
|
|
40
|
+
|
|
41
|
+
- **`Resolved here:`** — found and fixed inside this task. Name what proves it:
|
|
42
|
+
an `M<n>` check this task declares, or a repo-relative path that exists. If no
|
|
43
|
+
check covers it, the fix is not proved and the finding is one of the other two.
|
|
44
|
+
- **`Durable owner:`** — real, still open, and someone must do it. Name an
|
|
45
|
+
absolute `https://…` tracker reference or a repo-relative path that exists.
|
|
46
|
+
The reference must already carry the content it claims to hold.
|
|
47
|
+
- **`Discard reason:`** — considered, and deliberately not being done. Say why.
|
|
48
|
+
|
|
49
|
+
Picking the marker that passes rather than the one that is true is how a repair
|
|
50
|
+
gets filed as a dismissal, and the archive then records the opposite of what
|
|
51
|
+
happened.
|
|
35
52
|
|
|
36
53
|
The Review remains in tasks.md. A user-facing Report summarizes delivery but is not hidden gate state. Do not let Core or this checklist write evidence automatically.
|
|
37
54
|
|
|
@@ -43,6 +60,13 @@ When the change's artifacts or Touch extensions signal a domain, consult the mat
|
|
|
43
60
|
|
|
44
61
|
Each related critical expectation needs behavior evidence, a durable follow-up owner, or an explicit discard reason. Relevant `D<n>`, `F<n>`, `A<n>`, and `Q<n>` references in Covers must agree with their OpenSpec basis and resolution owner. Unresolved authority returns to OpenSpec authoring.
|
|
45
62
|
|
|
63
|
+
A durable owner declared as a URL must **already carry the content** it claims to hold, at the moment
|
|
64
|
+
it is cited. A valid link to an empty issue owns nothing: create the content, then reference it. Check
|
|
65
|
+
this **when it is cited**, not at archive — a check deferred to the close finds the same fact after
|
|
66
|
+
the reauthorization it should have prevented. This is **not a deterministic gate check** and must not
|
|
67
|
+
become one: a gate that fetched a reference would stop being local and offline, which is the property
|
|
68
|
+
its verdict rests on.
|
|
69
|
+
|
|
46
70
|
`keel/HANDOFF.md` is an optional pointer override and cannot own findings, critical expectation state, evidence details, or follow-ups.
|
|
47
71
|
|
|
48
72
|
## Skill change review
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: keel-run-single-task-goal
|
|
3
|
-
description: Use when the user explicitly authorizes automatic execution or resume of exactly one OpenSpec task on Codex or Claude. Drives the single-task native goal lifecycle through Keel gates, keeps the current agent the sole
|
|
3
|
+
description: Use when the user explicitly authorizes automatic execution or resume of exactly one OpenSpec task on Codex or Claude. Drives the single-task native goal lifecycle through Keel gates, keeps the current agent the sole holder of write authority, and stops at the task boundary. Never activate for ordinary apply, proposal work, ambiguous or multiple tasks, or OpenCode.
|
|
4
4
|
license: UNLICENSED
|
|
5
5
|
metadata:
|
|
6
6
|
keel-role: single-task-goal-activation
|
|
@@ -11,7 +11,7 @@ metadata:
|
|
|
11
11
|
|
|
12
12
|
## Purpose
|
|
13
13
|
|
|
14
|
-
Activate a native goal or subagent runtime to execute exactly one authorized OpenSpec task end to end, while OpenSpec, Git, the task-capsule fingerprint, and deterministic Keel gates stay the only durable authority. The current agent remains the sole
|
|
14
|
+
Activate a native goal or subagent runtime to execute exactly one authorized OpenSpec task end to end, while OpenSpec, Git, the task-capsule fingerprint, and deterministic Keel gates stay the only durable authority. The current agent remains the sole holder of write authority and owns Review, gate invocation, the task checkbox, and completion. Where delegation is declared, an authorized delegate may write inside the `Touch` boundary that authority already defined and acquires none of those decisions; the current agent re-runs each `M<n>` check itself before recording Evidence, because a delegate's reported result is a claim and the byte-identity check that validates a read-only helper cannot apply to a writer. A native evaluator declaring success never marks or reports the task complete.
|
|
15
15
|
|
|
16
16
|
## Authoritative sources and provenance
|
|
17
17
|
|
|
@@ -37,7 +37,7 @@ Activate only on an explicit, unambiguous request to automatically execute or re
|
|
|
37
37
|
- Proposal, design, or spec authoring before tasks are final.
|
|
38
38
|
- Ambiguous selection, multiple tasks, or a whole task group or change backlog.
|
|
39
39
|
- An unrelated native `/goal` use that is not a Keel OpenSpec task.
|
|
40
|
-
- Unrequested helpers or
|
|
40
|
+
- Unrequested helpers, or an undeclared delegation of implementation to another agent.
|
|
41
41
|
- OpenCode, which stays manual compatibility only with no v4 native activation.
|
|
42
42
|
|
|
43
43
|
If any of these hold, stop and use the normal manual Keel loop.
|
package/scripts/run_python.js
CHANGED
|
@@ -3,32 +3,108 @@
|
|
|
3
3
|
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
5
|
|
|
6
|
+
// The suite calls `tempfile.TemporaryDirectory(ignore_cleanup_errors=…)`,
|
|
7
|
+
// added in 3.10. Accepting any interpreter whose `--version` exits zero meant
|
|
8
|
+
// macOS system Python 3.9 ran the suite and failed ten scenarios with messages
|
|
9
|
+
// naming ten unrelated features — the reader is then debugging the suite
|
|
10
|
+
// instead of installing an interpreter.
|
|
11
|
+
//
|
|
12
|
+
// This is the one definition. `bin/keel.js` used to carry its own candidate
|
|
13
|
+
// list that asked only whether a command runs, so `keel --doctor` reported
|
|
14
|
+
// `python3: ok` for the same 3.9.6 this file refuses — two Keel surfaces
|
|
15
|
+
// answering one question with opposite verdicts, and the one a person runs
|
|
16
|
+
// deliberately to check their environment was the wrong one.
|
|
17
|
+
const MINIMUM_PYTHON = [3, 10];
|
|
18
|
+
|
|
19
|
+
// The newest minor worth looking for under a versioned name. Names are
|
|
20
|
+
// generated from MINIMUM_PYTHON up to this, newest first, so raising the
|
|
21
|
+
// minimum drops the names below it with no second edit; only this bound moves
|
|
22
|
+
// when a new Python is released.
|
|
23
|
+
const NEWEST_KNOWN_PYTHON_MINOR = 13;
|
|
24
|
+
|
|
25
|
+
// Versioned names matter because the unversioned one is often not the usable
|
|
26
|
+
// one. On macOS `python3` is the system 3.9 while Homebrew installs
|
|
27
|
+
// `python3.11` beside it, so a search that stops at `python3` refuses with
|
|
28
|
+
// "install a newer Python" addressed to someone who already did.
|
|
29
|
+
function versionedPythonNames() {
|
|
30
|
+
const names = [];
|
|
31
|
+
for (let minor = NEWEST_KNOWN_PYTHON_MINOR; minor >= MINIMUM_PYTHON[1]; minor -= 1) {
|
|
32
|
+
names.push({ command: `python${MINIMUM_PYTHON[0]}.${minor}`, prefixArgs: [] });
|
|
33
|
+
}
|
|
34
|
+
return names;
|
|
35
|
+
}
|
|
36
|
+
|
|
6
37
|
function pythonCandidates() {
|
|
38
|
+
// An explicit choice is not overridden by discovery.
|
|
7
39
|
if (process.env.KEEL_PYTHON) {
|
|
8
40
|
return [{ command: process.env.KEEL_PYTHON, prefixArgs: [] }];
|
|
9
41
|
}
|
|
10
42
|
|
|
43
|
+
// Unversioned names first, so a machine where the default interpreter
|
|
44
|
+
// already satisfies the minimum behaves exactly as it did before.
|
|
11
45
|
if (process.platform === "win32") {
|
|
12
46
|
return [
|
|
13
47
|
{ command: "py", prefixArgs: ["-3"] },
|
|
14
48
|
{ command: "python", prefixArgs: [] },
|
|
15
49
|
{ command: "python3", prefixArgs: [] },
|
|
50
|
+
...versionedPythonNames(),
|
|
16
51
|
];
|
|
17
52
|
}
|
|
18
53
|
|
|
19
54
|
return [
|
|
20
55
|
{ command: "python3", prefixArgs: [] },
|
|
21
56
|
{ command: "python", prefixArgs: [] },
|
|
57
|
+
...versionedPythonNames(),
|
|
22
58
|
];
|
|
23
59
|
}
|
|
24
60
|
|
|
25
|
-
function
|
|
61
|
+
function label(candidate) {
|
|
62
|
+
return [candidate.command, ...candidate.prefixArgs].join(" ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function reportedVersion(candidate) {
|
|
26
66
|
const result = spawnSync(
|
|
27
67
|
candidate.command,
|
|
28
68
|
[...candidate.prefixArgs, "--version"],
|
|
29
69
|
{ encoding: "utf8" }
|
|
30
70
|
);
|
|
31
|
-
|
|
71
|
+
if (result.error || result.status !== 0) return null;
|
|
72
|
+
// Python 2 wrote `--version` to stderr, so read both streams: an old
|
|
73
|
+
// interpreter should be reported by its version rather than dismissed as
|
|
74
|
+
// unreadable.
|
|
75
|
+
const match = `${result.stdout || ""}${result.stderr || ""}`.match(
|
|
76
|
+
/(\d+)\.(\d+)(?:\.(\d+))?/
|
|
77
|
+
);
|
|
78
|
+
return match ? match[0] : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function meetsMinimum(version) {
|
|
82
|
+
const parts = String(version || "").split(".").map(Number);
|
|
83
|
+
const [major, minor] = parts;
|
|
84
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor)) return false;
|
|
85
|
+
if (major !== MINIMUM_PYTHON[0]) return major > MINIMUM_PYTHON[0];
|
|
86
|
+
return minor >= MINIMUM_PYTHON[1];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Resolve the interpreter both surfaces should agree about. Returns the chosen
|
|
90
|
+
// candidate and every candidate tried with the version it reported, so the
|
|
91
|
+
// caller can either run it or report why there is nothing to run.
|
|
92
|
+
function resolveInterpreter() {
|
|
93
|
+
const tried = [];
|
|
94
|
+
for (const option of pythonCandidates()) {
|
|
95
|
+
const version = reportedVersion(option);
|
|
96
|
+
tried.push({ label: label(option), version });
|
|
97
|
+
if (version && meetsMinimum(version)) {
|
|
98
|
+
return { candidate: option, version, tried };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { candidate: null, version: null, tried };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function describeTried(tried) {
|
|
105
|
+
return tried
|
|
106
|
+
.map((entry) => `${entry.label} (${entry.version || "not runnable"})`)
|
|
107
|
+
.join(", ");
|
|
32
108
|
}
|
|
33
109
|
|
|
34
110
|
function main() {
|
|
@@ -38,10 +114,15 @@ function main() {
|
|
|
38
114
|
return 2;
|
|
39
115
|
}
|
|
40
116
|
|
|
41
|
-
const candidate =
|
|
117
|
+
const { candidate, tried } = resolveInterpreter();
|
|
42
118
|
if (!candidate) {
|
|
119
|
+
const minimum = MINIMUM_PYTHON.join(".");
|
|
43
120
|
process.stderr.write(
|
|
44
|
-
|
|
121
|
+
`run_python: no Python ${minimum} or newer was found, and the suite `
|
|
122
|
+
+ `needs one — tempfile.TemporaryDirectory(ignore_cleanup_errors=…) `
|
|
123
|
+
+ `was added in ${minimum}.\n`
|
|
124
|
+
+ `run_python: tried ${describeTried(tried) || "nothing"}.\n`
|
|
125
|
+
+ "run_python: install a newer Python, or point KEEL_PYTHON at one.\n"
|
|
45
126
|
);
|
|
46
127
|
return 127;
|
|
47
128
|
}
|
|
@@ -60,4 +141,19 @@ function main() {
|
|
|
60
141
|
return result.status ?? 1;
|
|
61
142
|
}
|
|
62
143
|
|
|
63
|
-
|
|
144
|
+
// `bin/keel.js` requires this file for the definitions above, so the script
|
|
145
|
+
// only runs when it is the entry point. Two copies of the interpreter rule is
|
|
146
|
+
// what this change exists to remove; importing is what keeps it at one.
|
|
147
|
+
module.exports = {
|
|
148
|
+
MINIMUM_PYTHON,
|
|
149
|
+
pythonCandidates,
|
|
150
|
+
versionedPythonNames,
|
|
151
|
+
reportedVersion,
|
|
152
|
+
meetsMinimum,
|
|
153
|
+
resolveInterpreter,
|
|
154
|
+
describeTried,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (require.main === module) {
|
|
158
|
+
process.exit(main());
|
|
159
|
+
}
|