@christang/keel 5.48.0 → 5.49.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/bin/keel.js
CHANGED
|
@@ -2234,4 +2234,11 @@ function main() {
|
|
|
2234
2234
|
return runAction(options);
|
|
2235
2235
|
}
|
|
2236
2236
|
|
|
2237
|
-
process.exit(
|
|
2237
|
+
// Not `process.exit()`: stdout to a pipe is asynchronous, and exiting discards
|
|
2238
|
+
// whatever the operating system has not yet accepted. Nothing is lost while the
|
|
2239
|
+
// payload fits the pipe buffer, which is why this was invisible at the 64KB
|
|
2240
|
+
// default — and under the memory pressure that shrinks buffers to a page or
|
|
2241
|
+
// two, a consumer receives a valid prefix of an incomplete document with no
|
|
2242
|
+
// error and no change of exit code. Setting the code instead lets the event
|
|
2243
|
+
// loop drain the write and end the process on its own.
|
|
2244
|
+
process.exitCode = main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.49.0",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.49.0",
|
|
4
4
|
"description": "Keel OpenSpec execution discipline: stateless continuity, task capsules, deterministic gates, and expectation alignment for Codex and Claude Code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TanglmChris",
|
|
@@ -37,8 +37,8 @@ REQUIRED_SCRIPTS = [
|
|
|
37
37
|
"scripts/validate_plugin.py",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
|
-
PACKAGE_VERSION = "5.
|
|
41
|
-
PROTOCOL_VERSION = "5.
|
|
40
|
+
PACKAGE_VERSION = "5.49.0"
|
|
41
|
+
PROTOCOL_VERSION = "5.49.0"
|
|
42
42
|
LEGACY_MANAGED_START = "<!-- keel:start version=2.1 -->"
|
|
43
43
|
OPENSPEC_SCHEMA_NAME = "keel-spec-driven"
|
|
44
44
|
# Mirrors KEEL_PACKAGE_NAME in scripts/install_to_repo.py, one of the two
|
|
@@ -24720,6 +24720,147 @@ def validate_marker_version_is_read_scenario() -> int:
|
|
|
24720
24720
|
return 0
|
|
24721
24721
|
|
|
24722
24722
|
|
|
24723
|
+
# `process.exit()` discards whatever Node has not flushed, and stdout to a pipe
|
|
24724
|
+
# is asynchronous. Whether anything is lost depends only on whether the payload
|
|
24725
|
+
# fit the pipe buffer — so at the 64KB default nothing is ever lost, and under
|
|
24726
|
+
# the memory pressure that shrinks buffers to a page or two, output is lost
|
|
24727
|
+
# silently. Waiting for that condition is not a test; this scenario creates it.
|
|
24728
|
+
def validate_output_survives_the_pipe_scenario() -> int:
|
|
24729
|
+
label = "output-survives-the-pipe"
|
|
24730
|
+
F_SETPIPE_SZ = 1031
|
|
24731
|
+
try:
|
|
24732
|
+
import fcntl
|
|
24733
|
+
import time
|
|
24734
|
+
except ImportError:
|
|
24735
|
+
return skip_scenario(
|
|
24736
|
+
label,
|
|
24737
|
+
"fcntl is unavailable, so the pipe buffer cannot be shrunk and the "
|
|
24738
|
+
"truncation cannot be forced rather than waited for",
|
|
24739
|
+
)
|
|
24740
|
+
|
|
24741
|
+
def goal_task(acceptance_padding: int) -> str:
|
|
24742
|
+
huge = ("Acceptance " + ("A" * acceptance_padding) + " is proven by M1.",)
|
|
24743
|
+
return _goal_tasks_file(
|
|
24744
|
+
[_goal_task_block(acceptance=huge, strategy="vertical-tdd")]
|
|
24745
|
+
)
|
|
24746
|
+
|
|
24747
|
+
goal_args = (
|
|
24748
|
+
"project", "goal",
|
|
24749
|
+
"--target", "codex",
|
|
24750
|
+
"--change", "sample-change", "--task", "1.1", "--json",
|
|
24751
|
+
)
|
|
24752
|
+
|
|
24753
|
+
with tempfile.TemporaryDirectory(prefix="keel-pipe-") as raw:
|
|
24754
|
+
root = Path(raw)
|
|
24755
|
+
repo = root / "repo"
|
|
24756
|
+
write_text(repo / "openspec/changes/sample-change/proposal.md", "# P\n")
|
|
24757
|
+
write_text(
|
|
24758
|
+
repo / "openspec/changes/sample-change/design.md",
|
|
24759
|
+
"## Context\n\nfixture\n",
|
|
24760
|
+
)
|
|
24761
|
+
write_text(
|
|
24762
|
+
repo / "openspec/changes/sample-change/specs/demo/spec.md",
|
|
24763
|
+
"## ADDED Requirements\n",
|
|
24764
|
+
)
|
|
24765
|
+
write_text(
|
|
24766
|
+
repo / "openspec/changes/sample-change/tasks.md", goal_task(4200)
|
|
24767
|
+
)
|
|
24768
|
+
|
|
24769
|
+
# The reference length: the same invocation with nowhere to lose bytes.
|
|
24770
|
+
redirected = run_keel(repo, *goal_args)
|
|
24771
|
+
if redirected.returncode != 0:
|
|
24772
|
+
report(
|
|
24773
|
+
f"{label}: the fixture did not produce a goal projection to "
|
|
24774
|
+
f"measure against; keel exited {redirected.returncode}."
|
|
24775
|
+
)
|
|
24776
|
+
report((redirected.stderr or redirected.stdout).strip()[:400])
|
|
24777
|
+
return 1
|
|
24778
|
+
expected = redirected.stdout.encode("utf-8")
|
|
24779
|
+
if len(expected) <= 4096:
|
|
24780
|
+
report(
|
|
24781
|
+
f"{label}: the fixture projection is {len(expected)} bytes, "
|
|
24782
|
+
"which fits the shrunken pipe, so the condition this scenario "
|
|
24783
|
+
"exists to force would not arise."
|
|
24784
|
+
)
|
|
24785
|
+
return 1
|
|
24786
|
+
|
|
24787
|
+
read_fd, write_fd = os.pipe()
|
|
24788
|
+
try:
|
|
24789
|
+
fcntl.fcntl(write_fd, F_SETPIPE_SZ, 4096)
|
|
24790
|
+
except OSError as error:
|
|
24791
|
+
os.close(read_fd)
|
|
24792
|
+
os.close(write_fd)
|
|
24793
|
+
return skip_scenario(
|
|
24794
|
+
label,
|
|
24795
|
+
"this platform refused F_SETPIPE_SZ "
|
|
24796
|
+
f"({error}), so the pipe buffer cannot be shrunk",
|
|
24797
|
+
)
|
|
24798
|
+
|
|
24799
|
+
child = subprocess.Popen(
|
|
24800
|
+
["node", str(ROOT / "bin" / "keel.js"), *goal_args],
|
|
24801
|
+
cwd=repo,
|
|
24802
|
+
stdout=write_fd,
|
|
24803
|
+
stderr=subprocess.DEVNULL,
|
|
24804
|
+
)
|
|
24805
|
+
os.close(write_fd)
|
|
24806
|
+
# Nothing is read for a moment, so the buffer fills and the rest of the
|
|
24807
|
+
# write is left pending. A child that discards its pending write on exit
|
|
24808
|
+
# is finished by now, having lost everything past the buffer; a child
|
|
24809
|
+
# that lets the event loop drain is blocked on the write and finishes
|
|
24810
|
+
# once the loop below starts reading. Reading immediately would drain
|
|
24811
|
+
# fast enough to let the broken form through, which is the race this
|
|
24812
|
+
# scenario exists to remove — so the pause is the forcing condition, and
|
|
24813
|
+
# the child is waited for after the read rather than before it.
|
|
24814
|
+
time.sleep(1.0)
|
|
24815
|
+
received = b""
|
|
24816
|
+
while True:
|
|
24817
|
+
chunk = os.read(read_fd, 65536)
|
|
24818
|
+
if not chunk:
|
|
24819
|
+
break
|
|
24820
|
+
received += chunk
|
|
24821
|
+
os.close(read_fd)
|
|
24822
|
+
child.wait(timeout=60)
|
|
24823
|
+
|
|
24824
|
+
if received != expected:
|
|
24825
|
+
report(
|
|
24826
|
+
f"{label}: {len(received)} of {len(expected)} bytes survived a "
|
|
24827
|
+
"4096-byte pipe. Output written for a program to read is being "
|
|
24828
|
+
"discarded at process exit, with no error and no exit-code "
|
|
24829
|
+
"change — the consumer receives a valid prefix of an "
|
|
24830
|
+
"incomplete document."
|
|
24831
|
+
)
|
|
24832
|
+
return 1
|
|
24833
|
+
try:
|
|
24834
|
+
json.loads(received.decode("utf-8"))
|
|
24835
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
24836
|
+
report(f"{label}: the payload that survived does not parse: {error}.")
|
|
24837
|
+
return 1
|
|
24838
|
+
|
|
24839
|
+
# D3: the one behavioral difference a reader would worry about.
|
|
24840
|
+
ok = run_keel(repo, "--version")
|
|
24841
|
+
if ok.returncode != 0:
|
|
24842
|
+
report(f"{label}: a successful command returned {ok.returncode}.")
|
|
24843
|
+
return 1
|
|
24844
|
+
refused = run_keel(
|
|
24845
|
+
repo, "gate", "task-start", "--change", "no-such-change", "--task", "1.1"
|
|
24846
|
+
)
|
|
24847
|
+
if refused.returncode != 1:
|
|
24848
|
+
report(
|
|
24849
|
+
f"{label}: a refused gate returned {refused.returncode}, not 1."
|
|
24850
|
+
)
|
|
24851
|
+
return 1
|
|
24852
|
+
invalid = run_keel(repo, "--not-a-flag")
|
|
24853
|
+
if invalid.returncode != 2:
|
|
24854
|
+
report(
|
|
24855
|
+
f"{label}: an invalid argument returned {invalid.returncode}, "
|
|
24856
|
+
"not 2."
|
|
24857
|
+
)
|
|
24858
|
+
return 1
|
|
24859
|
+
|
|
24860
|
+
report(f"{label} scenario passed.")
|
|
24861
|
+
return 0
|
|
24862
|
+
|
|
24863
|
+
|
|
24723
24864
|
# A scenario name, as the registry spells one. Two registered names carry no
|
|
24724
24865
|
# hyphen — `cli` and `uninstall` — so requiring one would leave exactly those
|
|
24725
24866
|
# two unchecked, and allowing single words was measured to add no false
|
|
@@ -24950,6 +25091,7 @@ SCENARIOS: tuple = (
|
|
|
24950
25091
|
("cli", validate_cli_scenario),
|
|
24951
25092
|
("doctor-openspec-honesty", validate_doctor_openspec_honesty_scenario),
|
|
24952
25093
|
("the-marker-version-is-read", validate_marker_version_is_read_scenario),
|
|
25094
|
+
("output-survives-the-pipe", validate_output_survives_the_pipe_scenario),
|
|
24953
25095
|
(
|
|
24954
25096
|
"authored-scenario-names-are-registered",
|
|
24955
25097
|
validate_authored_scenario_names_scenario,
|