@christang/keel 5.7.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.
@@ -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 writer, and stops at the task boundary. Never activate for ordinary apply, proposal work, ambiguous or multiple tasks, or OpenCode.
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 writer and owns Review, gate invocation, the task checkbox, and completion. A native evaluator declaring success never marks or reports the task complete.
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 any request to delegate implementation to another agent.
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.
@@ -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 commandExists(candidate) {
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
- return !result.error && result.status === 0;
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 = pythonCandidates().find(commandExists);
117
+ const { candidate, tried } = resolveInterpreter();
42
118
  if (!candidate) {
119
+ const minimum = MINIMUM_PYTHON.join(".");
43
120
  process.stderr.write(
44
- "run_python: could not find Python. Install python3/python or set KEEL_PYTHON.\n"
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
- process.exit(main());
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
+ }