@christang/keel 5.7.0 → 5.16.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.
@@ -21,7 +21,9 @@ 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
- - When no trustworthy explicit Git base exists, do not attribute dirty paths automatically. Review scope semantically.
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.
26
+ - Scope attribution is now the gate's job by default: `keel gate task-complete` compares the worktree against the dirty set `task-start` recorded and refuses a path outside Touch, so a `pass` is evidence about scope rather than silence about it. Two cases still need you. When the gate reports paths as *unattributed* — no recorded set, because the manifest predates the field or the guard was cleared — nothing was checked and the review is the only scope evidence. And a path already dirty when the task started is never attributed even if the task changed it again, so read the gate's silence about such a path as absence of a check rather than absence of a write.
25
27
  - For `Coupling: required`, confirm one complete candidate reached its completion gate and generated artifacts are aligned.
26
28
 
27
29
  ## Semantic Review
@@ -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 each unresolved finding with a durable OpenSpec task/new change, archive-evidence owner, or explicit discard rationale.
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 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
+ }