@christang/keel 5.55.0 → 5.57.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
@@ -313,6 +313,37 @@ keel --uninstall # reverts core.hooksPath when Keel set it
313
313
  `--with-git-hooks` is opt-in: a plain `keel --install` never touches git config, and the override
314
314
  is repo-local and reversible.
315
315
 
316
+ ## Declaring what a red proves
317
+
318
+ Red-green discipline makes you write a check that fails before the implementation exists, and
319
+ `keel gate task-complete` refuses a task whose `M<n>.red` Evidence is missing. What it could not
320
+ check was *why* the red failed — a red that fails because a fixture is empty, or because `PATH` was
321
+ never cleared, is shape-perfect.
322
+
323
+ A check may close by declaring the failure its red must show:
324
+
325
+ ```
326
+ - M1: `npm test -- --scenario widget` asserts the widget renders. Fails with: `widget is undefined`
327
+ ```
328
+
329
+ Completion then requires that literal in the check's `.red` Evidence:
330
+
331
+ ```
332
+ - M1.red: fail, for the right reason. The scenario reported `widget is undefined`.
333
+ ```
334
+
335
+ The clause lives inside the check text, so it is inside the contract fingerprint. That is the point:
336
+ a signature cannot be added or rewritten after the red was observed without the anchor moving, which
337
+ is what separates a prediction from a transcription. Keel does not judge whether the signature is a
338
+ good one — you can declare a string that any failure prints — it holds only that you wrote it first
339
+ and that review can see it.
340
+
341
+ The clause is optional; a check without one behaves exactly as before. A declaration on a check that
342
+ can have no red — one tagged `(regression)`, or any check under a strategy outside the red-green
343
+ set — fails `task-start` by name rather than sitting in the contract doing nothing. So does a
344
+ `Fails with:` marker that names no literal. To *write about* the marker in a check without declaring
345
+ one, put it in inline code.
346
+
316
347
  ## Domain lenses
317
348
 
318
349
  Keel's core is pure process; it ships no domain knowledge and no decisions of its own. Alongside
@@ -1,4 +1,4 @@
1
- <!-- keel:start version=5.55.0 -->
1
+ <!-- keel:start version=5.57.0 -->
2
2
  ## Keel Bootstrap
3
3
 
4
4
  - Start every session with `keel context`; OpenSpec artifacts and Git are the only durable authority — never native memory, goals, or transcripts.
package/bin/keel.js CHANGED
@@ -94,7 +94,8 @@ Usage:
94
94
  keel capabilities [repo] [--target claude|codex|opencode] [--json]
95
95
  keel project [repo] --target claude|codex|opencode --event startup|resume|compaction|goal|task-view|worktree|subagent-start|subagent-stop [--authorize goal|task-view|subagent] [--expected-owner owner] [--native-complete] [--change name] [--task id] [--json]
96
96
  keel project tasks [repo] --target claude [--change name] [--json]
97
- keel gate task-start|task-complete|change-close [repo] [--change name] [--task id] [--action sync|archive] [--base git-ref] [--no-guard] [--record] [--json]
97
+ keel gate task-start|task-complete [repo] [--change name] [--task id] [--base git-ref] [--no-guard] [--record] [--keep-evidence M1,M3] [--json]
98
+ keel gate change-close [repo] [--change name] --action sync|archive [--base git-ref] [--json]
98
99
  keel guard start|status|clear [repo] [--change name] [--task id] [--force] [--json]
99
100
  keel lenses list|add [name] [repo] [--force]
100
101
  keel triage [repo] [--labels <l1,l2>] [--issue <n>] [--json]
@@ -1194,6 +1195,8 @@ function keelOpenSpecOverlay(action) {
1194
1195
  "",
1195
1196
  "Keel rules below take precedence over conflicting generic OpenSpec instructions in this file.",
1196
1197
  "",
1198
+ "- Invoke the OpenSpec CLI as `keel openspec …` throughout this file. The commands below are written as a bare `openspec`, which resolves only where OpenSpec is separately installed on PATH; `keel openspec` resolves either way, and `keel --doctor` reports which case this repository is.",
1199
+ "",
1197
1200
  "### Expectation alignment before specs and tasks finalize",
1198
1201
  "",
1199
1202
  "- Before specs and executable tasks are finalized, run `keel-align-expectations`: quick path for complete low-risk requests, deep path when a material choice can change user-visible behavior, an external interface, acceptance, security/privacy/permission boundaries, data migration, protocol/state/timing/reset semantics, generated equivalence, irreversible cost, or a dependency commitment.",
@@ -1229,6 +1232,8 @@ function keelOpenSpecOverlay(action) {
1229
1232
  "",
1230
1233
  "Keel rules below take precedence over conflicting generic OpenSpec instructions in this file.",
1231
1234
  "",
1235
+ "- Invoke the OpenSpec CLI as `keel openspec …` throughout this file. The commands below are written as a bare `openspec`, which resolves only where OpenSpec is separately installed on PATH; `keel openspec` resolves either way, and `keel --doctor` reports which case this repository is.",
1236
+ "",
1232
1237
  ...syncBody,
1233
1238
  OPENSPEC_SURFACE_OVERLAY_END,
1234
1239
  "",
@@ -1269,6 +1274,8 @@ function keelOpenSpecOverlay(action) {
1269
1274
  "",
1270
1275
  "Keel rules below take precedence over conflicting generic OpenSpec instructions in this file.",
1271
1276
  "",
1277
+ "- Invoke the OpenSpec CLI as `keel openspec …` throughout this file. The commands below are written as a bare `openspec`, which resolves only where OpenSpec is separately installed on PATH; `keel openspec` resolves either way, and `keel --doctor` reports which case this repository is.",
1278
+ "",
1272
1279
  "### Target-native subagent gate",
1273
1280
  "",
1274
1281
  "- The current agent remains responsible for Keel ownership, task/archive decisions, scope control, and final reporting.",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@christang/keel",
3
3
  "displayName": "Keel",
4
4
  "description": "Keel OpenSpec execution discipline CLI for Claude Code, Codex, and OpenCode.",
5
- "version": "5.55.0",
5
+ "version": "5.57.0",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keel",
3
- "version": "5.55.0",
3
+ "version": "5.57.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.55.0",
3
+ "version": "5.57.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.55.0"
41
- PROTOCOL_VERSION = "5.55.0"
40
+ PACKAGE_VERSION = "5.57.0"
41
+ PROTOCOL_VERSION = "5.57.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
@@ -2163,7 +2163,7 @@ def validate_authoring_continuity_scenario() -> int:
2163
2163
  or scaffold_payload.get("status") != "ready"
2164
2164
  or scaffold_payload.get("selection")
2165
2165
  != {"source": "inferred", "change": "draft", "task": None}
2166
- or scaffold_payload.get("nextAction") != {"kind": "author"}
2166
+ or scaffold_payload.get("nextAction", {}).get("kind") != "author"
2167
2167
  ):
2168
2168
  report(
2169
2169
  "authoring-continuity scenario did not keep an incomplete proposal actionable."
@@ -3747,8 +3747,14 @@ def validate_stateless_continuity_scenario() -> int:
3747
3747
  "change": "demo",
3748
3748
  "task": "1.1",
3749
3749
  },
3750
- "nextAction": {"kind": "task-start"},
3751
3750
  }
3751
+ expected_kind = "task-start"
3752
+ if payload.get("nextAction", {}).get("kind") != expected_kind:
3753
+ report(
3754
+ "stateless-continuity scenario explicit selection reported "
3755
+ f"next action {payload.get('nextAction')!r}."
3756
+ )
3757
+ return 1
3752
3758
  for key, value in expected.items():
3753
3759
  if payload.get(key) != value:
3754
3760
  report(
@@ -3808,7 +3814,7 @@ def validate_stateless_continuity_scenario() -> int:
3808
3814
  inferred_payload.get("status") != "ready"
3809
3815
  or inferred_payload.get("selection")
3810
3816
  != {"source": "inferred", "change": "only", "task": "1.1"}
3811
- or inferred_payload.get("nextAction") != {"kind": "task-start"}
3817
+ or inferred_payload.get("nextAction", {}).get("kind") != "task-start"
3812
3818
  ):
3813
3819
  report("stateless-continuity scenario unique inference mismatch.")
3814
3820
  report(inferred.stdout.strip())
@@ -3830,7 +3836,8 @@ def validate_stateless_continuity_scenario() -> int:
3830
3836
  or recomputed_payload.get("status") != "ready"
3831
3837
  or recomputed_payload.get("selection")
3832
3838
  != {"source": "inferred", "change": "only", "task": None}
3833
- or recomputed_payload.get("nextAction") != {"kind": "change-close"}
3839
+ or recomputed_payload.get("nextAction", {}).get("kind")
3840
+ != "change-close"
3834
3841
  ):
3835
3842
  report("stateless-continuity scenario did not recompute completed state.")
3836
3843
  report((recomputed.stderr or recomputed.stdout).strip())
@@ -3849,7 +3856,7 @@ def validate_stateless_continuity_scenario() -> int:
3849
3856
  evidence_payload = json.loads(evidence_ready.stdout)
3850
3857
  if (
3851
3858
  evidence_ready.returncode != 0
3852
- or evidence_payload.get("nextAction") != {"kind": "task-complete"}
3859
+ or evidence_payload.get("nextAction", {}).get("kind") != "task-complete"
3853
3860
  ):
3854
3861
  report("stateless-continuity scenario missed evidence-ready completion.")
3855
3862
  report((evidence_ready.stderr or evidence_ready.stdout).strip())
@@ -3902,7 +3909,7 @@ def validate_stateless_continuity_scenario() -> int:
3902
3909
  or handoff_payload.get("status") != "ready"
3903
3910
  or handoff_payload.get("selection")
3904
3911
  != {"source": "handoff", "change": "beta", "task": "1.1"}
3905
- or handoff_payload.get("nextAction") != {"kind": "task-start"}
3912
+ or handoff_payload.get("nextAction", {}).get("kind") != "task-start"
3906
3913
  ):
3907
3914
  report("stateless-continuity scenario did not prioritize valid HANDOFF.")
3908
3915
  report((handoff.stderr or handoff.stdout).strip())
@@ -4036,7 +4043,7 @@ def validate_stateless_continuity_scenario() -> int:
4036
4043
  discuss = run_keel(discuss_repo, "context", "--json")
4037
4044
  if (
4038
4045
  discuss.returncode != 0
4039
- or json.loads(discuss.stdout).get("nextAction") != {"kind": "discuss"}
4046
+ or json.loads(discuss.stdout).get("nextAction", {}).get("kind") != "discuss"
4040
4047
  ):
4041
4048
  report("stateless-continuity scenario missed discuss transition.")
4042
4049
  report((discuss.stderr or discuss.stdout).strip())
@@ -4051,7 +4058,7 @@ def validate_stateless_continuity_scenario() -> int:
4051
4058
  author = run_keel(author_repo, "context", "--json")
4052
4059
  if (
4053
4060
  author.returncode != 0
4054
- or json.loads(author.stdout).get("nextAction") != {"kind": "author"}
4061
+ or json.loads(author.stdout).get("nextAction", {}).get("kind") != "author"
4055
4062
  ):
4056
4063
  report("stateless-continuity scenario missed author transition.")
4057
4064
  report((author.stderr or author.stdout).strip())
@@ -4069,7 +4076,7 @@ def validate_stateless_continuity_scenario() -> int:
4069
4076
  idle.returncode != 0
4070
4077
  or idle_payload.get("status") != "idle"
4071
4078
  or idle_payload.get("selection") is not None
4072
- or idle_payload.get("nextAction") != {"kind": "none"}
4079
+ or idle_payload.get("nextAction", {}).get("kind") != "none"
4073
4080
  ):
4074
4081
  report("stateless-continuity scenario no-work state was not idle.")
4075
4082
  report((idle.stderr or idle.stdout).strip())
@@ -4227,7 +4234,7 @@ def validate_stateless_continuity_scenario() -> int:
4227
4234
  if (
4228
4235
  matching.returncode != 0
4229
4236
  or matching_payload.get("status") != "ready"
4230
- or matching_payload.get("nextAction") != {"kind": "task-start"}
4237
+ or matching_payload.get("nextAction", {}).get("kind") != "task-start"
4231
4238
  ):
4232
4239
  report("stateless-continuity scenario did not resume a matching anchor.")
4233
4240
  report((matching.stderr or matching.stdout).strip())
@@ -4315,7 +4322,7 @@ def validate_stateless_continuity_scenario() -> int:
4315
4322
  or matching_handoff_payload.get("status") != "ready"
4316
4323
  or matching_handoff_payload.get("selection")
4317
4324
  != {"source": "handoff", "change": "demo", "task": "1.1"}
4318
- or matching_handoff_payload.get("nextAction") != {"kind": "task-start"}
4325
+ or matching_handoff_payload.get("nextAction", {}).get("kind") != "task-start"
4319
4326
  ):
4320
4327
  report("stateless-continuity scenario did not resume a matching HANDOFF anchor.")
4321
4328
  report((matching_handoff.stderr or matching_handoff.stdout).strip())
@@ -25013,6 +25020,306 @@ def problem_codes(payload: dict) -> list[str]:
25013
25020
  return [str(entry.get("code", "")) for entry in (payload.get("problems") or [])]
25014
25021
 
25015
25022
 
25023
+
25024
+ # Red-green forces the expensive half — a check that fails before the
25025
+ # implementation exists — and enforces that a `.red` line is present. Nothing
25026
+ # checked that the red failed *because of the thing under test*. Issue #116
25027
+ # measured two reds in one session that were shape-perfect and failed for an
25028
+ # unrelated reason: one where the fixture had not cleared PATH so the check
25029
+ # passed, one where the fixture contained no wording the checker would refuse.
25030
+ # The declaration is what a gate can hold: written into the check, so it is in
25031
+ # the fingerprint, and therefore written before the run rather than after it.
25032
+ def validate_red_declares_what_it_proves_scenario() -> int:
25033
+ label = "a-red-declares-what-it-proves"
25034
+ signature = "openspec: not found"
25035
+ declared = (
25036
+ f"M1: node test.js asserts the public behavior. Fails with: `{signature}`",
25037
+ )
25038
+ plain = ("M1: node test.js asserts the public behavior",)
25039
+ with tempfile.TemporaryDirectory(prefix="keel-failswith-") as raw:
25040
+ root = Path(raw)
25041
+
25042
+ def start(name: str, **kwargs) -> dict:
25043
+ return strategy_probe_start(
25044
+ root, name, strategy_probe_task(**kwargs)
25045
+ )
25046
+
25047
+ def commands_of(payload: dict) -> list[dict]:
25048
+ contract = payload.get("contract") or {}
25049
+ capsule = contract.get("capsule") or {}
25050
+ return ((capsule.get("verification") or {}).get("commands") or [])
25051
+
25052
+ def fingerprint_of(payload: dict) -> str:
25053
+ return str((payload.get("contract") or {}).get("fingerprint") or "")
25054
+
25055
+ # A declared signature compiles and is carried as its own field, so the
25056
+ # gate can read it without re-parsing the author's prose.
25057
+ with_signature = start(
25058
+ "declared", strategy="vertical-tdd", commands=declared
25059
+ )
25060
+ if with_signature.get("status") != "pass":
25061
+ report(
25062
+ f"{label}: a check declaring a failure signature was refused; "
25063
+ f"got {with_signature.get('status')!r} "
25064
+ f"{problem_text(with_signature)!r}."
25065
+ )
25066
+ return 1
25067
+ compiled = commands_of(with_signature)
25068
+ if len(compiled) != 1 or compiled[0].get("failsWith") != signature:
25069
+ report(
25070
+ f"{label}: the compiled check does not carry the declared "
25071
+ f"failure signature; got {compiled!r}."
25072
+ )
25073
+ return 1
25074
+ # The clause is part of the check the author wrote, so it stays in
25075
+ # `check` as well; the field is what the gate reads, not a replacement
25076
+ # for the text.
25077
+ if "Fails with:" not in str(compiled[0].get("check", "")):
25078
+ report(
25079
+ f"{label}: compiling the signature stripped it out of the "
25080
+ f"check text the author wrote; got {compiled[0]!r}."
25081
+ )
25082
+ return 1
25083
+
25084
+ # In the fingerprint for free, which is what makes the declaration a
25085
+ # prediction rather than a transcription: it cannot be retrofitted to
25086
+ # whatever the run happened to print without the anchor moving.
25087
+ edited = start(
25088
+ "edited",
25089
+ strategy="vertical-tdd",
25090
+ commands=(
25091
+ "M1: node test.js asserts the public behavior. "
25092
+ "Fails with: `a different failure entirely`",
25093
+ ),
25094
+ )
25095
+ without = start("without", strategy="vertical-tdd", commands=plain)
25096
+ prints = {
25097
+ "declared": fingerprint_of(with_signature),
25098
+ "edited": fingerprint_of(edited),
25099
+ "without": fingerprint_of(without),
25100
+ }
25101
+ if not all(prints.values()):
25102
+ report(f"{label}: a probe reported no fingerprint; got {prints!r}.")
25103
+ return 1
25104
+ if prints["declared"] == prints["edited"]:
25105
+ report(
25106
+ f"{label}: editing only the declared failure signature left "
25107
+ f"the fingerprint unmoved at {prints['declared']!r}, so a "
25108
+ "signature could be rewritten after the red was recorded."
25109
+ )
25110
+ return 1
25111
+ if prints["declared"] == prints["without"]:
25112
+ report(
25113
+ f"{label}: declaring a signature left the fingerprint "
25114
+ f"unmoved at {prints['without']!r}."
25115
+ )
25116
+ return 1
25117
+
25118
+ # A declaration that can produce no red is refused rather than left to
25119
+ # sit in the contract doing nothing, which reads to its author as a
25120
+ # check being enforced.
25121
+ for name, kwargs in (
25122
+ (
25123
+ "regression",
25124
+ dict(
25125
+ strategy="vertical-tdd",
25126
+ commands=(
25127
+ "M1: node test.js asserts the public behavior",
25128
+ "M2 (regression): the suite stays green. "
25129
+ f"Fails with: `{signature}`",
25130
+ ),
25131
+ ),
25132
+ ),
25133
+ (
25134
+ "no-red-strategy",
25135
+ dict(
25136
+ strategy="evidence-first",
25137
+ reason="the artifact is inspected after it is written.",
25138
+ commands=declared,
25139
+ ),
25140
+ ),
25141
+ ):
25142
+ payload = start(name, **kwargs)
25143
+ if payload.get("status") != "fail":
25144
+ report(
25145
+ f"{label}: a failure signature on a check with no red "
25146
+ f"({name}) was accepted; got {payload.get('status')!r}."
25147
+ )
25148
+ return 1
25149
+ text = problem_text(payload)
25150
+ expected_label = "M2" if name == "regression" else "M1"
25151
+ if expected_label not in text or "red" not in text:
25152
+ report(
25153
+ f"{label}: the refusal for {name} does not name the check "
25154
+ f"and say it records no red; got {text!r}."
25155
+ )
25156
+ return 1
25157
+ if "signature-without-red" not in problem_codes(payload):
25158
+ report(
25159
+ f"{label}: the refusal for {name} carries no "
25160
+ f"signature-without-red code; got "
25161
+ f"{problem_codes(payload)!r}."
25162
+ )
25163
+ return 1
25164
+
25165
+ # A marker that names no literal is the shape of a typo. Ignoring it
25166
+ # leaves the author believing a signature is enforced when none parsed.
25167
+ for name, check in (
25168
+ ("bare", "M1: node test.js asserts the behavior. Fails with:"),
25169
+ (
25170
+ "unquoted",
25171
+ "M1: node test.js asserts the behavior. Fails with: not found",
25172
+ ),
25173
+ (
25174
+ "not-closing",
25175
+ "M1: node test.js asserts it. Fails with: `not found` "
25176
+ "and then keeps going.",
25177
+ ),
25178
+ ):
25179
+ payload = start(name, strategy="vertical-tdd", commands=(check,))
25180
+ if payload.get("status") != "fail":
25181
+ report(
25182
+ f"{label}: a malformed signature marker ({name}) compiled "
25183
+ f"as if nothing were declared; got "
25184
+ f"{payload.get('status')!r}."
25185
+ )
25186
+ return 1
25187
+ if "malformed-failure-signature" not in problem_codes(payload):
25188
+ report(
25189
+ f"{label}: the refusal for {name} carries no "
25190
+ f"malformed-failure-signature code; got "
25191
+ f"{problem_codes(payload)!r}."
25192
+ )
25193
+ return 1
25194
+
25195
+ # The escape is the one this repository already established for quoted
25196
+ # material: a check that describes this rule has to be able to name it.
25197
+ quoted = start(
25198
+ "quoted",
25199
+ strategy="vertical-tdd",
25200
+ commands=(
25201
+ "M1: a `Fails with:` marker with no literal is refused by name",
25202
+ ),
25203
+ )
25204
+ if quoted.get("status") != "pass":
25205
+ report(
25206
+ f"{label}: a marker written inside inline code was read as a "
25207
+ f"declaration; got {quoted.get('status')!r} "
25208
+ f"{problem_text(quoted)!r}."
25209
+ )
25210
+ return 1
25211
+ if commands_of(quoted)[0].get("failsWith") is not None:
25212
+ report(
25213
+ f"{label}: a marker inside inline code compiled to a failure "
25214
+ f"signature; got {commands_of(quoted)!r}."
25215
+ )
25216
+ return 1
25217
+
25218
+ # The obligation is stated where the author is deciding — before the
25219
+ # failing check is written — rather than discovered once it has run.
25220
+ warnings = " ".join(str(w) for w in (with_signature.get("warnings") or []))
25221
+ if signature not in warnings or "M1" not in warnings:
25222
+ report(
25223
+ f"{label}: the task-start obligation does not name which "
25224
+ f"checks declared a failure signature; got {warnings!r}."
25225
+ )
25226
+ return 1
25227
+ undeclared = " ".join(str(w) for w in (without.get("warnings") or []))
25228
+ if "declared no failure signature" not in undeclared:
25229
+ report(
25230
+ f"{label}: the obligation does not say that a check owing a "
25231
+ f"red declared no signature; got {undeclared!r}."
25232
+ )
25233
+ return 1
25234
+
25235
+ # Completion is where the declaration becomes a criterion.
25236
+ repo = root / "completion"
25237
+ tasks_path = repo / "openspec/changes/demo/tasks.md"
25238
+
25239
+ def complete(check: str, red: str) -> dict:
25240
+ write_gate_fixture(
25241
+ repo,
25242
+ tasks=strategy_probe_task(
25243
+ strategy="vertical-tdd", commands=(check,)
25244
+ ).replace(
25245
+ " - M1: pending\n",
25246
+ f" - M1: the check ran.\n"
25247
+ f" - M1.red: {red}\n"
25248
+ " - M1.green: it passed after the implementation.\n",
25249
+ ),
25250
+ )
25251
+ record_contract_anchor(repo, "demo")
25252
+ result = run_keel(
25253
+ repo,
25254
+ "gate",
25255
+ "task-complete",
25256
+ "--change",
25257
+ "demo",
25258
+ "--task",
25259
+ "1.1",
25260
+ "--json",
25261
+ )
25262
+ try:
25263
+ return json.loads(result.stdout)
25264
+ except json.JSONDecodeError:
25265
+ return {
25266
+ "status": "unparsed",
25267
+ "problems": [{"message": result.stdout[:400]}],
25268
+ }
25269
+
25270
+ declared_check = declared[0]
25271
+ mismatched = complete(
25272
+ declared_check, "it failed before the implementation existed."
25273
+ )
25274
+ if mismatched.get("status") != "fail":
25275
+ report(
25276
+ f"{label}: task-complete accepted a red that does not show "
25277
+ f"the declared failure; got {mismatched.get('status')!r}."
25278
+ )
25279
+ return 1
25280
+ text = problem_text(mismatched)
25281
+ if "M1" not in text or signature not in text:
25282
+ report(
25283
+ f"{label}: the refusal names neither the check nor the string "
25284
+ f"it did not find; got {text!r}."
25285
+ )
25286
+ return 1
25287
+ if "red-missing-declared-failure" not in problem_codes(mismatched):
25288
+ report(
25289
+ f"{label}: the refusal carries no "
25290
+ f"red-missing-declared-failure code; got "
25291
+ f"{problem_codes(mismatched)!r}."
25292
+ )
25293
+ return 1
25294
+
25295
+ matched = complete(
25296
+ declared_check,
25297
+ f"it failed with `{signature}` before the implementation existed.",
25298
+ )
25299
+ if matched.get("status") != "pass":
25300
+ report(
25301
+ f"{label}: a red showing the declared failure was refused; "
25302
+ f"got {matched.get('status')!r} {problem_text(matched)!r}."
25303
+ )
25304
+ return 1
25305
+
25306
+ # D3: the undeclared case is unchanged, asserted against the same
25307
+ # fixture with the clause removed and the same weak red that the
25308
+ # declared check was refused for.
25309
+ undeclared_task = complete(
25310
+ plain[0], "it failed before the implementation existed."
25311
+ )
25312
+ if undeclared_task.get("status") != "pass":
25313
+ report(
25314
+ f"{label}: a red-green task declaring no signature stopped "
25315
+ f"completing; got {undeclared_task.get('status')!r} "
25316
+ f"{problem_text(undeclared_task)!r}."
25317
+ )
25318
+ return 1
25319
+
25320
+ report(f"{label} scenario passed.")
25321
+ return 0
25322
+
25016
25323
  # The strategy was the one capsule field the compiler supplied from a value no
25017
25324
  # spec documents as a default — and it supplied the weakest of the six, so
25018
25325
  # omitting the line was how a task opted out of red-green.
@@ -26119,6 +26426,190 @@ def validate_evidence_survives_what_did_not_change_scenario() -> int:
26119
26426
  return 0
26120
26427
 
26121
26428
 
26429
+ # `keel context` exists to answer "what now" and answered with a noun. Issue
26430
+ # #112: `Next action: change-close` followed by `keel gate change-close
26431
+ # --change x` failing on the argument that stage requires — a wrong attempt
26432
+ # Keel had everything in the same result to prevent.
26433
+ def validate_next_action_is_a_command_scenario() -> int:
26434
+ label = "the-next-action-is-a-command"
26435
+
26436
+ def repo_with(root: Path, name: str, *, checked: bool, evidence: bool):
26437
+ repo = root / name
26438
+ task = strategy_probe_task(
26439
+ strategy="evidence-first",
26440
+ reason="fixture; nothing here can fail first",
26441
+ )
26442
+ if checked:
26443
+ task = task.replace("- [ ] 1.1", "- [x] 1.1")
26444
+ if evidence:
26445
+ task = task.replace(" - M1: pending", " - M1: pass. ran it.")
26446
+ write_gate_fixture(repo, tasks=task)
26447
+ started = run_keel(
26448
+ repo, "gate", "task-start", "--change", "demo", "--task", "1.1",
26449
+ "--json", "--no-guard",
26450
+ )
26451
+ value = json.loads(started.stdout)["contract"]["fingerprint"]["value"]
26452
+ tasks_path = repo / "openspec/changes/demo/tasks.md"
26453
+ tasks_path.write_text(
26454
+ tasks_path.read_text(encoding="utf-8").replace(
26455
+ " - Contract: pending",
26456
+ f" - Contract: keel-task-capsule/v1 sha256:{value}",
26457
+ ),
26458
+ encoding="utf-8",
26459
+ )
26460
+ return repo
26461
+
26462
+ def context(repo: Path):
26463
+ text = run_keel(repo, "context", "--change", "demo").stdout
26464
+ payload = json.loads(
26465
+ run_keel(repo, "context", "--change", "demo", "--json").stdout
26466
+ )
26467
+ return text, payload
26468
+
26469
+ with tempfile.TemporaryDirectory(prefix="keel-next-command-") as raw:
26470
+ root = Path(raw)
26471
+
26472
+ pending = repo_with(root, "pending", checked=False, evidence=False)
26473
+ text, payload = context(pending)
26474
+ command = (payload.get("nextAction") or {}).get("command")
26475
+ if not command:
26476
+ report(
26477
+ f"{label}: the next action carries no command; "
26478
+ f"{payload.get('nextAction')!r}."
26479
+ )
26480
+ return 1
26481
+ for needed in ("task-start", "--change demo", "--task 1.1"):
26482
+ if needed not in command:
26483
+ report(
26484
+ f"{label}: the reported command omits {needed!r}; "
26485
+ f"{command!r}."
26486
+ )
26487
+ return 1
26488
+ if command not in text:
26489
+ report(
26490
+ f"{label}: the text surface does not carry the same command as "
26491
+ f"--json; {command!r} not in {text!r}."
26492
+ )
26493
+ return 1
26494
+
26495
+ done = repo_with(root, "done", checked=False, evidence=True)
26496
+ _, payload = context(done)
26497
+ if "task-complete" not in str((payload.get("nextAction") or {}).get("command")):
26498
+ report(
26499
+ f"{label}: a task with completion evidence did not report the "
26500
+ f"task-complete invocation; {payload.get('nextAction')!r}."
26501
+ )
26502
+ return 1
26503
+
26504
+ closing = repo_with(root, "closing", checked=True, evidence=True)
26505
+ _, payload = context(closing)
26506
+ close_command = str((payload.get("nextAction") or {}).get("command") or "")
26507
+ if "change-close" not in close_command:
26508
+ report(
26509
+ f"{label}: a change whose tasks are complete did not report the "
26510
+ f"change-close invocation; {payload.get('nextAction')!r}."
26511
+ )
26512
+ return 1
26513
+ if "--action" not in close_command:
26514
+ report(
26515
+ f"{label}: the change-close command omits the argument that "
26516
+ f"stage requires; {close_command!r}."
26517
+ )
26518
+ return 1
26519
+ # Running exactly what was printed must not hit the input error the
26520
+ # report describes.
26521
+ printed = close_command.split()
26522
+ if printed and printed[0] == "keel":
26523
+ printed = printed[1:]
26524
+ ran = run_keel(closing, *printed)
26525
+ if "requires --action" in (ran.stdout + ran.stderr):
26526
+ report(
26527
+ f"{label}: the reported command still fails on the argument it "
26528
+ f"was supposed to supply; {(ran.stdout + ran.stderr)[:200]!r}."
26529
+ )
26530
+ return 1
26531
+
26532
+ idle = root / "idle"
26533
+ write_text(idle / "README.md", "# fixture\n")
26534
+ payload = json.loads(run_keel(idle, "context", "--json").stdout)
26535
+ if (payload.get("nextAction") or {}).get("command"):
26536
+ report(
26537
+ f"{label}: an action with nothing to run reported a command; "
26538
+ f"{payload.get('nextAction')!r}."
26539
+ )
26540
+ return 1
26541
+
26542
+ report(f"{label} scenario passed.")
26543
+ return 0
26544
+
26545
+
26546
+ # `keel --init` writes OpenSpec's command and skill surfaces, whose text says
26547
+ # `openspec new change "<name>"`. With only the Keel package installed globally
26548
+ # a bare `openspec` is not on PATH, which `keel --doctor` reports and names the
26549
+ # working invocation for. Those files belong to OpenSpec and Keel appends a
26550
+ # block rather than editing their body — so the block is where Keel says it.
26551
+ def validate_overlay_names_the_invocation_scenario() -> int:
26552
+ label = "the-overlay-names-the-invocation"
26553
+ start = "<!-- keel:openspec-surface-overlay"
26554
+ end = "<!-- keel:openspec-surface-overlay:end -->"
26555
+ with tempfile.TemporaryDirectory(prefix="keel-overlay-invocation-") as raw:
26556
+ repo = Path(raw)
26557
+ # The surfaces OpenSpec writes, as OpenSpec writes them: the overlay is
26558
+ # merged into files that already exist, and their body is the text this
26559
+ # scenario asserts Keel does not touch.
26560
+ upstream = 'Run `openspec new change "<name>"` to begin.\n'
26561
+ for action in ("propose", "apply", "sync", "archive"):
26562
+ write_text(repo / f".claude/commands/opsx/{action}.md", upstream)
26563
+ for skill in (
26564
+ "openspec-propose",
26565
+ "openspec-apply-change",
26566
+ "openspec-sync-specs",
26567
+ "openspec-archive-change",
26568
+ ):
26569
+ write_text(repo / f".claude/skills/{skill}/SKILL.md", upstream)
26570
+ installed = run_keel(repo, "--install", "--target", "claude")
26571
+ if installed.returncode != 0:
26572
+ report(
26573
+ f"{label}: the fixture install failed; "
26574
+ f"{(installed.stderr or installed.stdout).strip()[:300]}"
26575
+ )
26576
+ return 1
26577
+
26578
+ carrying = [
26579
+ path
26580
+ for path in sorted(repo.rglob("*.md"))
26581
+ if start in path.read_text(encoding="utf-8", errors="replace")
26582
+ ]
26583
+ if not carrying:
26584
+ report(f"{label}: the install wrote no file carrying an overlay.")
26585
+ return 1
26586
+ for path in carrying:
26587
+ content = path.read_text(encoding="utf-8")
26588
+ block = content[content.index(start):content.index(end) + len(end)]
26589
+ for needed in ("keel openspec", "keel --doctor"):
26590
+ if needed not in block:
26591
+ report(
26592
+ f"{label}: the overlay in {path.name} does not name "
26593
+ f"{needed!r}."
26594
+ )
26595
+ return 1
26596
+ # The boundary: everything outside the block is OpenSpec's.
26597
+ outside = (
26598
+ content[: content.index(start)]
26599
+ + content[content.index(end) + len(end):]
26600
+ )
26601
+ if start in outside or "keel openspec" in outside:
26602
+ report(
26603
+ f"{label}: {path.name} carries Keel invocation text outside "
26604
+ "the overlay block, so the OpenSpec-authored body was "
26605
+ "edited."
26606
+ )
26607
+ return 1
26608
+
26609
+ report(f"{label} scenario passed.")
26610
+ return 0
26611
+
26612
+
26122
26613
  # A scenario name, as the registry spells one. Two registered names carry no
26123
26614
  # hyphen — `cli` and `uninstall` — so requiring one would leave exactly those
26124
26615
  # two unchecked, and allowing single words was measured to add no false
@@ -26351,6 +26842,7 @@ SCENARIOS: tuple = (
26351
26842
  ("the-marker-version-is-read", validate_marker_version_is_read_scenario),
26352
26843
  ("output-survives-the-pipe", validate_output_survives_the_pipe_scenario),
26353
26844
  ("a-strategy-is-declared", validate_strategy_is_declared_scenario),
26845
+ ("a-red-declares-what-it-proves", validate_red_declares_what_it_proves_scenario),
26354
26846
  ("the-weakest-strategy-states-its-reason", validate_weakest_strategy_states_its_reason_scenario),
26355
26847
  ("a-quoted-marker-is-not-a-disposition", validate_quoted_marker_is_not_a_disposition_scenario),
26356
26848
  ("drift-names-where-to-look", validate_drift_names_where_to_look_scenario),
@@ -26359,6 +26851,8 @@ SCENARIOS: tuple = (
26359
26851
  ("an-explanation-is-printed-once", validate_explanation_is_printed_once_scenario),
26360
26852
  ("a-paused-change-is-not-the-next-action", validate_paused_change_is_not_the_next_action_scenario),
26361
26853
  ("evidence-survives-what-did-not-change", validate_evidence_survives_what_did_not_change_scenario),
26854
+ ("the-next-action-is-a-command", validate_next_action_is_a_command_scenario),
26855
+ ("the-overlay-names-the-invocation", validate_overlay_names_the_invocation_scenario),
26362
26856
  (
26363
26857
  "authored-scenario-names-are-registered",
26364
26858
  validate_authored_scenario_names_scenario,
@@ -29,12 +29,34 @@ function taskRecords(tasksPath) {
29
29
  }));
30
30
  }
31
31
 
32
+ // The invocation the action's name stands for. Everything it needs is in the
33
+ // same result, and the failure it prevents is the one issue #112 reports: an
34
+ // action name followed by an attempt that fails on an argument Keel knew was
35
+ // required. Change and task are named explicitly rather than left to
36
+ // inference, because a reader may run the printed line later or elsewhere,
37
+ // where inference would answer about a different repository state.
38
+ function nextActionCommand(kind, selection) {
39
+ if (!selection || !selection.change) return null;
40
+ const scope = `--change ${selection.change}`;
41
+ if (kind === "task-start" || kind === "task-complete") {
42
+ if (!selection.task) return null;
43
+ return `keel gate ${kind} ${scope} --task ${selection.task}`;
44
+ }
45
+ if (kind === "change-close") {
46
+ // `--action` is not optional for this stage, so a command printed without
47
+ // it is a command that fails.
48
+ return `keel gate change-close ${scope} --action archive`;
49
+ }
50
+ return null;
51
+ }
52
+
32
53
  function result(status, selection, nextAction, read, reasons = [], contract = null) {
54
+ const command = nextActionCommand(nextAction, selection);
33
55
  const context = {
34
56
  schemaVersion: 1,
35
57
  status,
36
58
  selection,
37
- nextAction: { kind: nextAction },
59
+ nextAction: command ? { kind: nextAction, command } : { kind: nextAction },
38
60
  read,
39
61
  reasons,
40
62
  warnings: [],
@@ -679,6 +701,9 @@ function renderContext(result) {
679
701
  `Keel context: ${result.status}`,
680
702
  `Next action: ${result.nextAction.kind}`,
681
703
  ];
704
+ if (result.nextAction.command) {
705
+ lines.push(`Run: ${result.nextAction.command}`);
706
+ }
682
707
  if (result.selection) {
683
708
  lines.push(
684
709
  `Selection: ${result.selection.change}`
package/src/core/gates.js CHANGED
@@ -232,9 +232,24 @@ function redGreenObligation(compiled) {
232
232
  const strategy = compiled.capsule.verification.strategy.toLowerCase();
233
233
  if (!RED_GREEN_VERIFICATION_STRATEGIES.has(strategy)) return [];
234
234
  const commands = compiled.capsule.verification.commands;
235
- const owing = commands.filter((item) => !item.regression).map((item) => item.label);
235
+ const owingChecks = commands.filter((item) => !item.regression);
236
+ const owing = owingChecks.map((item) => item.label);
236
237
  const exempt = commands.filter((item) => item.regression).map((item) => item.label);
237
238
  if (owing.length === 0) return [];
239
+ // What a red proves is only checkable if it was written down before the red
240
+ // was run, so the moment to say so is here — the author is about to write the
241
+ // failing check — and not at completion, where any signature is a
242
+ // transcription of whatever already failed (issue #116).
243
+ const signed = owingChecks.filter((item) => item.failsWith);
244
+ const unsigned = owingChecks.filter((item) => !item.failsWith).map((item) => item.label);
245
+ const signatures = signed.length > 0
246
+ ? ` ${signed.map((item) => `${item.label} must fail with \`${item.failsWith}\``).join("; ")}.`
247
+ + (unsigned.length > 0
248
+ ? ` ${unsigned.join(", ")} declared no failure signature.`
249
+ : "")
250
+ : ` ${owing.join(", ")} declared no failure signature — close the check with `
251
+ + "`Fails with:` and the literal its red should print, and the red that "
252
+ + "gets recorded has to be the one you predicted.";
238
253
  return [
239
254
  `${strategy} will require concrete .red and .green Evidence at completion `
240
255
  + `for ${owing.join(", ")}`
@@ -244,7 +259,8 @@ function redGreenObligation(compiled) {
244
259
  : "")
245
260
  + ". Tag a check `(regression)` now if it asserts that something already "
246
261
  + "green stays green, rather than discovering the obligation once the "
247
- + "checks have been run.",
262
+ + "checks have been run."
263
+ + signatures,
248
264
  ];
249
265
  }
250
266
 
@@ -1011,8 +1027,34 @@ function completionChecks(repo, task, contract = null, changeVerify = null, chan
1011
1027
  .filter((entry) => entry.regression)
1012
1028
  .map((entry) => entry.label)
1013
1029
  );
1030
+ // A check may declare the failure its red must show. Enforced against the
1031
+ // recorded `.red`, which is where the shape check stopped: a red that
1032
+ // failed for an unrelated reason satisfied the presence check exactly as
1033
+ // well as one that failed for the right reason (issue #116). Keel does not
1034
+ // judge whether the declared string is a good one — what it holds is that
1035
+ // the string is in the contract, so it was written before the run.
1036
+ const declaredFailure = new Map(
1037
+ (contract ? contract.capsule.verification.commands : [])
1038
+ .filter((entry) => entry.failsWith)
1039
+ .map((entry) => [entry.label, entry.failsWith])
1040
+ );
1014
1041
  for (const label of commands) {
1015
1042
  if (exempt.has(label)) continue;
1043
+ const expected = declaredFailure.get(label);
1044
+ const recorded = evidenceValue(task, `${label}.red`);
1045
+ if (expected && isConcrete(recorded) && !String(recorded).includes(expected)) {
1046
+ problems.push(
1047
+ problem(
1048
+ "red-missing-declared-failure",
1049
+ `${label} declares that its red fails with \`${expected}\`, and `
1050
+ + `the recorded ${label}.red Evidence does not contain that `
1051
+ + "string. Record the failure output the red actually printed, "
1052
+ + "or correct the declaration — which moves the contract "
1053
+ + "fingerprint, because a signature edited after the red was "
1054
+ + "observed is a transcription of it.",
1055
+ )
1056
+ );
1057
+ }
1016
1058
  for (const phase of ["red", "green"]) {
1017
1059
  if (!isConcrete(evidenceValue(task, `${label}.${phase}`))) {
1018
1060
  problems.push(
@@ -153,6 +153,31 @@ const RED_GREEN_VERIFICATION_STRATEGIES = new Set([
153
153
  // Tags an M<n> check may carry after its label, as a comma-separated set.
154
154
  const COMMAND_TAGS = new Set(["fast", "full", "regression"]);
155
155
 
156
+ // A check may end by declaring the failure its red is expected to show, so that
157
+ // what the red proves is written down before the red is run. The clause closes
158
+ // the check: `Fails with:` followed by one inline-code literal and nothing more.
159
+ // End-anchored on purpose — a check that describes this rule mentions the marker
160
+ // mid-sentence, and a mention is not a declaration.
161
+ const FAILURE_SIGNATURE = /\bFails with:[ \t]*`([^`\n]+)`[ \t]*$/i;
162
+ const FAILURE_MARKER = /\bFails with:/i;
163
+
164
+ // Classify a check's `Fails with:` marker. `signature` is the declared literal;
165
+ // `malformed` marks a marker that is present and is not a closing clause — a
166
+ // typo shape, and ignoring it would leave the author believing a signature is
167
+ // enforced when none was parsed. A marker written inside inline code is quoted
168
+ // material rather than a declaration, the meaning inline code already carries
169
+ // here, which is what lets this file's own tasks name the marker.
170
+ function failureSignature(check) {
171
+ const text = String(check || "");
172
+ const match = text.match(FAILURE_SIGNATURE);
173
+ if (match) return { signature: match[1].trim(), malformed: false };
174
+ const remainder = withoutInlineCode(text);
175
+ return {
176
+ signature: null,
177
+ malformed: FAILURE_MARKER.test(remainder),
178
+ };
179
+ }
180
+
156
181
  // Single source of truth for the accepted completion Review `Status`
157
182
  // vocabulary. Consumed by both the completion gate (src/core/gates.js) and the
158
183
  // context "already reviewed" probe (src/core/context.js) so the two never
@@ -193,19 +218,39 @@ function verification(task) {
193
218
  // therefore exempt from the red-green evidence requirement. A check may
194
219
  // carry both, so the tag is a comma-separated set rather than one word.
195
220
  const match = entry.match(/^(M[1-9]\d*)(?:\s*\(([^)\n]*)\))?:\s*(.*)$/);
196
- if (!match) return { label: null, layer: "full", regression: false, check: entry };
221
+ if (!match) {
222
+ return {
223
+ label: null,
224
+ layer: "full",
225
+ regression: false,
226
+ check: entry,
227
+ failsWith: null,
228
+ malformedSignature: false,
229
+ };
230
+ }
197
231
  const tags = (match[2] || "")
198
232
  .split(",")
199
233
  .map((tag) => tag.trim().toLowerCase())
200
234
  .filter(Boolean);
201
235
  if (tags.some((tag) => !COMMAND_TAGS.has(tag))) {
202
- return { label: null, layer: "full", regression: false, check: entry };
236
+ return {
237
+ label: null,
238
+ layer: "full",
239
+ regression: false,
240
+ check: entry,
241
+ failsWith: null,
242
+ malformedSignature: false,
243
+ };
203
244
  }
245
+ const check = normalizeText(match[3]);
246
+ const failure = failureSignature(check);
204
247
  return {
205
248
  label: match[1],
206
249
  layer: tags.includes("fast") ? "fast" : "full",
207
250
  regression: tags.includes("regression"),
208
- check: normalizeText(match[3]),
251
+ check,
252
+ failsWith: failure.signature,
253
+ malformedSignature: failure.malformed,
209
254
  };
210
255
  });
211
256
  return {
@@ -367,7 +412,60 @@ function taskStartContractProblems(task) {
367
412
  ...regressionOnlyProblems(task),
368
413
  ];
369
414
  }
370
- return [...commandLabelProblems(task), ...regressionOnlyProblems(task)];
415
+ return [
416
+ ...commandLabelProblems(task),
417
+ ...regressionOnlyProblems(task),
418
+ ...failureSignatureProblems(task),
419
+ ];
420
+ }
421
+
422
+ // A declared failure signature describes the check's red. A `(regression)` check
423
+ // is exempt from red-green and a strategy outside the red-green set records no
424
+ // `.red` at all, so in both cases the declaration describes evidence that will
425
+ // never exist. Refused rather than left sitting in the contract: a declaration
426
+ // doing nothing reads to its author as a check being enforced.
427
+ function failureSignatureProblems(task) {
428
+ const parsed = verification(task);
429
+ const strategy = parsed.strategy.toLowerCase();
430
+ const redGreen = RED_GREEN_VERIFICATION_STRATEGIES.has(strategy);
431
+ const problems = [];
432
+ for (const entry of parsed.commands) {
433
+ if (!entry.label) continue;
434
+ if (entry.malformedSignature) {
435
+ problems.push({
436
+ code: "malformed-failure-signature",
437
+ message:
438
+ `${entry.label} carries a \`Fails with:\` marker that does not close `
439
+ + "the check with a literal. Write the failure signature as one "
440
+ + "inline-code literal at the end of the check, or fence the marker "
441
+ + "in inline code when the check is describing it rather than "
442
+ + "declaring one.",
443
+ });
444
+ continue;
445
+ }
446
+ if (!entry.failsWith) continue;
447
+ if (!redGreen) {
448
+ problems.push({
449
+ code: "signature-without-red",
450
+ message:
451
+ `${entry.label} declares the failure its red must show, but `
452
+ + `\`${parsed.strategy}\` records no red for the signature to `
453
+ + "describe. Name a red-green strategy, or drop the clause.",
454
+ });
455
+ continue;
456
+ }
457
+ if (entry.regression) {
458
+ problems.push({
459
+ code: "signature-without-red",
460
+ message:
461
+ `${entry.label} declares the failure its red must show, but it is `
462
+ + "tagged `(regression)` and so records no red for the signature to "
463
+ + "describe. Untag the check if it proves new behavior, or drop the "
464
+ + "clause.",
465
+ });
466
+ }
467
+ }
468
+ return problems;
371
469
  }
372
470
 
373
471
  // A red-green strategy whose every check is exempt from red-green is that
@@ -1127,6 +1225,10 @@ function compileTaskContract(repo, change, task) {
1127
1225
  const emitted = { label: entry.label, check: entry.check };
1128
1226
  if (entry.layer && entry.layer !== "full") emitted.layer = entry.layer;
1129
1227
  if (entry.regression) emitted.regression = true;
1228
+ // The clause stays in `check` — it is text the author wrote — and the
1229
+ // field is what the gate reads. Emitted only when declared, so every
1230
+ // check without one keeps the capsule shape and fingerprint it had.
1231
+ if (entry.failsWith) emitted.failsWith = entry.failsWith;
1130
1232
  return emitted;
1131
1233
  }),
1132
1234
  },