@christang/keel 5.47.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/README.md
CHANGED
|
@@ -280,8 +280,10 @@ command at the right moment. Three things make that happen.
|
|
|
280
280
|
- **The hooks.** A SessionStart hook runs the continuity projection the moment a session
|
|
281
281
|
opens; a PreToolUse hook enforces the write guard on every edit. Neither needs prompting.
|
|
282
282
|
|
|
283
|
-
So in day-to-day use you run two commands: `keel --
|
|
284
|
-
|
|
283
|
+
So in day-to-day use you run two commands: `keel --doctor` when you want to check the
|
|
284
|
+
wiring, and `keel --init` whenever it tells you the repository is behind its install — the
|
|
285
|
+
protocol version lives in your `AGENTS.md`, and updating the package does not move it.
|
|
286
|
+
Everything below is the vocabulary the agent uses on your behalf.
|
|
285
287
|
|
|
286
288
|
## Verification layering
|
|
287
289
|
|
package/bin/keel.js
CHANGED
|
@@ -983,6 +983,78 @@ function printDoctorLine(name, status, detail = "") {
|
|
|
983
983
|
process.stdout.write(`${name}: ${status}${detail ? ` - ${detail}` : ""}\n`);
|
|
984
984
|
}
|
|
985
985
|
|
|
986
|
+
// The `version=` attribute of the managed marker is written by every install
|
|
987
|
+
// and, until this line existed, read back by nothing that runs locally: both
|
|
988
|
+
// marker parsers match `keel:start(?:\s+[^>]*)?` and throw the attributes away.
|
|
989
|
+
// The one reader was the plugin's SessionStart hook, and doctor reports that
|
|
990
|
+
// plugin's activation as manual on every target — so the check might simply not
|
|
991
|
+
// be running, with nothing to distinguish that from agreement. Doctor is the
|
|
992
|
+
// model-free fallback: the declaration comes from the working tree and the
|
|
993
|
+
// running version from this process, so the comparison holds with no plugin at
|
|
994
|
+
// all.
|
|
995
|
+
function declaredProtocolVersion(repo) {
|
|
996
|
+
const agentsPath = path.join(repo, "AGENTS.md");
|
|
997
|
+
if (!fs.existsSync(agentsPath)) return null;
|
|
998
|
+
const match = fs
|
|
999
|
+
.readFileSync(agentsPath, "utf8")
|
|
1000
|
+
.match(/<!--\s*keel:start\s+version=(\d+\.\d+\.\d+)\s*-->/);
|
|
1001
|
+
return match ? match[1] : null;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Numeric, not lexical: "5.9.0" precedes "5.10.0" and string order says
|
|
1005
|
+
// otherwise. Returns a negative number when `a` is behind `b`.
|
|
1006
|
+
function compareVersions(a, b) {
|
|
1007
|
+
const left = a.split(".").map(Number);
|
|
1008
|
+
const right = b.split(".").map(Number);
|
|
1009
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1010
|
+
if (left[i] !== right[i]) return left[i] - right[i];
|
|
1011
|
+
}
|
|
1012
|
+
return 0;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Printed on every run, agreeing or not. A check that is silent when it passes
|
|
1016
|
+
// cannot be told apart from a check that did not run, which is the failure this
|
|
1017
|
+
// line exists to remove — so silence is never the report.
|
|
1018
|
+
function printProtocolVersionDrift(repo, target) {
|
|
1019
|
+
const running = PACKAGE_JSON.version;
|
|
1020
|
+
const declared = declaredProtocolVersion(repo);
|
|
1021
|
+
if (!declared) {
|
|
1022
|
+
printDoctorLine(
|
|
1023
|
+
"protocol",
|
|
1024
|
+
"not comparable",
|
|
1025
|
+
`this CLI is ${running}; the repository declares no protocol version in `
|
|
1026
|
+
+ "an AGENTS.md `keel:start` marker — run "
|
|
1027
|
+
+ `keel --init --target ${target} to write one`
|
|
1028
|
+
);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const order = compareVersions(declared, running);
|
|
1032
|
+
if (order === 0) {
|
|
1033
|
+
printDoctorLine(
|
|
1034
|
+
"protocol",
|
|
1035
|
+
"ok",
|
|
1036
|
+
`repo declares ${declared}, this CLI is ${running}`
|
|
1037
|
+
);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
// The two directions have different repairs, and the wrong repair is a no-op
|
|
1041
|
+
// that reads as a failure, so the line names the direction rather than the
|
|
1042
|
+
// difference. Drift never reaches the exit code: an out-of-date install is
|
|
1043
|
+
// not a broken one, and a doctor that goes red on release day is a doctor
|
|
1044
|
+
// people switch off.
|
|
1045
|
+
printDoctorLine(
|
|
1046
|
+
"protocol",
|
|
1047
|
+
"warning",
|
|
1048
|
+
order < 0
|
|
1049
|
+
? `repo declares ${declared}, this CLI is ${running} — the repository is `
|
|
1050
|
+
+ `behind its install; run keel --init --target ${target} to bring the `
|
|
1051
|
+
+ "protocol forward"
|
|
1052
|
+
: `repo declares ${declared}, this CLI is ${running} — the install is `
|
|
1053
|
+
+ "behind the repository, which carries a protocol this CLI cannot "
|
|
1054
|
+
+ "enforce; update the Keel package"
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
986
1058
|
function codexHome() {
|
|
987
1059
|
const configured = (process.env.CODEX_HOME || "").trim();
|
|
988
1060
|
return path.resolve(configured || path.join(os.homedir(), ".codex"));
|
|
@@ -1553,6 +1625,8 @@ function runDoctor(options) {
|
|
|
1553
1625
|
);
|
|
1554
1626
|
}
|
|
1555
1627
|
|
|
1628
|
+
printProtocolVersionDrift(repo, options.target);
|
|
1629
|
+
|
|
1556
1630
|
process.stdout.write("\nProject status:\n");
|
|
1557
1631
|
const checkStatus = runPython(
|
|
1558
1632
|
INSTALL_SCRIPT,
|
|
@@ -2160,4 +2234,11 @@ function main() {
|
|
|
2160
2234
|
return runAction(options);
|
|
2161
2235
|
}
|
|
2162
2236
|
|
|
2163
|
-
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
|
|
@@ -24597,6 +24597,270 @@ def validate_doctor_openspec_honesty_scenario() -> int:
|
|
|
24597
24597
|
return 0
|
|
24598
24598
|
|
|
24599
24599
|
|
|
24600
|
+
# The marker's `version=` attribute is written by the installer and, before this
|
|
24601
|
+
# scenario, read back by nothing that runs locally: both marker parsers match
|
|
24602
|
+
# `keel:start(?:\s+[^>]*)?` and discard the attributes. The only reader was the
|
|
24603
|
+
# plugin's SessionStart hook, whose activation doctor itself reports as manual.
|
|
24604
|
+
def validate_marker_version_is_read_scenario() -> int:
|
|
24605
|
+
def doctor_protocol_line(repo: Path) -> tuple[str, int]:
|
|
24606
|
+
result = run_keel(repo, "--doctor")
|
|
24607
|
+
line = next(
|
|
24608
|
+
(
|
|
24609
|
+
entry
|
|
24610
|
+
for entry in result.stdout.splitlines()
|
|
24611
|
+
if entry.startswith("protocol:")
|
|
24612
|
+
),
|
|
24613
|
+
"",
|
|
24614
|
+
)
|
|
24615
|
+
return line, result.returncode
|
|
24616
|
+
|
|
24617
|
+
def write_repo(root: Path, name: str, marker: str | None) -> Path:
|
|
24618
|
+
repo = root / name
|
|
24619
|
+
repo.mkdir()
|
|
24620
|
+
body = "# Agents\n\nSession Start.\n"
|
|
24621
|
+
(repo / "AGENTS.md").write_text(
|
|
24622
|
+
f"{marker}\n{body}" if marker is not None else body,
|
|
24623
|
+
encoding="utf-8",
|
|
24624
|
+
)
|
|
24625
|
+
return repo
|
|
24626
|
+
|
|
24627
|
+
with tempfile.TemporaryDirectory(prefix="keel-marker-version-") as raw:
|
|
24628
|
+
root = Path(raw)
|
|
24629
|
+
|
|
24630
|
+
behind = write_repo(root, "behind", "<!-- keel:start version=5.14.0 -->")
|
|
24631
|
+
line, behind_code = doctor_protocol_line(behind)
|
|
24632
|
+
if not line.startswith("protocol: warning"):
|
|
24633
|
+
report(
|
|
24634
|
+
"the-marker-version-is-read scenario: a repository declaring an "
|
|
24635
|
+
f"older protocol did not warn; got {line!r}."
|
|
24636
|
+
)
|
|
24637
|
+
return 1
|
|
24638
|
+
for needed in ("5.14.0", PACKAGE_VERSION, "keel --init"):
|
|
24639
|
+
if needed not in line:
|
|
24640
|
+
report(
|
|
24641
|
+
"the-marker-version-is-read scenario: the behind-repository "
|
|
24642
|
+
f"warning omits {needed!r}; got {line!r}."
|
|
24643
|
+
)
|
|
24644
|
+
return 1
|
|
24645
|
+
if "repository" not in line:
|
|
24646
|
+
report(
|
|
24647
|
+
"the-marker-version-is-read scenario: the warning does not name "
|
|
24648
|
+
f"the repository as the term that is behind; got {line!r}."
|
|
24649
|
+
)
|
|
24650
|
+
return 1
|
|
24651
|
+
|
|
24652
|
+
ahead = write_repo(root, "ahead", "<!-- keel:start version=9.99.0 -->")
|
|
24653
|
+
line, _ = doctor_protocol_line(ahead)
|
|
24654
|
+
if not line.startswith("protocol: warning"):
|
|
24655
|
+
report(
|
|
24656
|
+
"the-marker-version-is-read scenario: a repository declaring a "
|
|
24657
|
+
f"newer protocol did not warn; got {line!r}."
|
|
24658
|
+
)
|
|
24659
|
+
return 1
|
|
24660
|
+
for needed in ("9.99.0", PACKAGE_VERSION, "install"):
|
|
24661
|
+
if needed not in line:
|
|
24662
|
+
report(
|
|
24663
|
+
"the-marker-version-is-read scenario: the ahead-repository "
|
|
24664
|
+
f"warning omits {needed!r}; got {line!r}."
|
|
24665
|
+
)
|
|
24666
|
+
return 1
|
|
24667
|
+
if "keel --init" in line:
|
|
24668
|
+
report(
|
|
24669
|
+
"the-marker-version-is-read scenario: a repository ahead of its "
|
|
24670
|
+
f"install was told to re-run keel --init; got {line!r}."
|
|
24671
|
+
)
|
|
24672
|
+
return 1
|
|
24673
|
+
|
|
24674
|
+
agreed = write_repo(
|
|
24675
|
+
root, "agreed", f"<!-- keel:start version={PACKAGE_VERSION} -->"
|
|
24676
|
+
)
|
|
24677
|
+
line, agreed_code = doctor_protocol_line(agreed)
|
|
24678
|
+
if not line.startswith("protocol: ok"):
|
|
24679
|
+
report(
|
|
24680
|
+
"the-marker-version-is-read scenario: agreeing versions did not "
|
|
24681
|
+
f"report ok; got {line!r}."
|
|
24682
|
+
)
|
|
24683
|
+
return 1
|
|
24684
|
+
if line.count(PACKAGE_VERSION) < 2:
|
|
24685
|
+
report(
|
|
24686
|
+
"the-marker-version-is-read scenario: the agreeing line does not "
|
|
24687
|
+
f"print both versions; got {line!r}."
|
|
24688
|
+
)
|
|
24689
|
+
return 1
|
|
24690
|
+
|
|
24691
|
+
# D2: drift is a warning and nothing else. A line that could turn a
|
|
24692
|
+
# pipeline red on release day is a line users switch off.
|
|
24693
|
+
if behind_code != agreed_code:
|
|
24694
|
+
report(
|
|
24695
|
+
"the-marker-version-is-read scenario: drift changed doctor's "
|
|
24696
|
+
f"exit code ({behind_code} vs {agreed_code})."
|
|
24697
|
+
)
|
|
24698
|
+
return 1
|
|
24699
|
+
|
|
24700
|
+
for name, marker in (
|
|
24701
|
+
("undeclared", None),
|
|
24702
|
+
("attributeless", "<!-- keel:start -->"),
|
|
24703
|
+
):
|
|
24704
|
+
repo = write_repo(root, name, marker)
|
|
24705
|
+
line, _ = doctor_protocol_line(repo)
|
|
24706
|
+
if not line.startswith("protocol: not comparable"):
|
|
24707
|
+
report(
|
|
24708
|
+
f"the-marker-version-is-read scenario: the {name} repository "
|
|
24709
|
+
f"did not report `not comparable`; got {line!r}."
|
|
24710
|
+
)
|
|
24711
|
+
return 1
|
|
24712
|
+
if "declares" not in line:
|
|
24713
|
+
report(
|
|
24714
|
+
f"the-marker-version-is-read scenario: the {name} repository "
|
|
24715
|
+
f"was not told which term is missing; got {line!r}."
|
|
24716
|
+
)
|
|
24717
|
+
return 1
|
|
24718
|
+
|
|
24719
|
+
report("the-marker-version-is-read scenario passed.")
|
|
24720
|
+
return 0
|
|
24721
|
+
|
|
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
|
+
|
|
24600
24864
|
# A scenario name, as the registry spells one. Two registered names carry no
|
|
24601
24865
|
# hyphen — `cli` and `uninstall` — so requiring one would leave exactly those
|
|
24602
24866
|
# two unchecked, and allowing single words was measured to add no false
|
|
@@ -24826,6 +25090,8 @@ SCENARIOS: tuple = (
|
|
|
24826
25090
|
),
|
|
24827
25091
|
("cli", validate_cli_scenario),
|
|
24828
25092
|
("doctor-openspec-honesty", validate_doctor_openspec_honesty_scenario),
|
|
25093
|
+
("the-marker-version-is-read", validate_marker_version_is_read_scenario),
|
|
25094
|
+
("output-survives-the-pipe", validate_output_survives_the_pipe_scenario),
|
|
24829
25095
|
(
|
|
24830
25096
|
"authored-scenario-names-are-registered",
|
|
24831
25097
|
validate_authored_scenario_names_scenario,
|