@christang/keel 5.55.0 → 5.56.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
|
@@ -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
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.56.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.56.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.56.0"
|
|
41
|
+
PROTOCOL_VERSION = "5.56.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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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())
|
|
@@ -26119,6 +26126,190 @@ def validate_evidence_survives_what_did_not_change_scenario() -> int:
|
|
|
26119
26126
|
return 0
|
|
26120
26127
|
|
|
26121
26128
|
|
|
26129
|
+
# `keel context` exists to answer "what now" and answered with a noun. Issue
|
|
26130
|
+
# #112: `Next action: change-close` followed by `keel gate change-close
|
|
26131
|
+
# --change x` failing on the argument that stage requires — a wrong attempt
|
|
26132
|
+
# Keel had everything in the same result to prevent.
|
|
26133
|
+
def validate_next_action_is_a_command_scenario() -> int:
|
|
26134
|
+
label = "the-next-action-is-a-command"
|
|
26135
|
+
|
|
26136
|
+
def repo_with(root: Path, name: str, *, checked: bool, evidence: bool):
|
|
26137
|
+
repo = root / name
|
|
26138
|
+
task = strategy_probe_task(
|
|
26139
|
+
strategy="evidence-first",
|
|
26140
|
+
reason="fixture; nothing here can fail first",
|
|
26141
|
+
)
|
|
26142
|
+
if checked:
|
|
26143
|
+
task = task.replace("- [ ] 1.1", "- [x] 1.1")
|
|
26144
|
+
if evidence:
|
|
26145
|
+
task = task.replace(" - M1: pending", " - M1: pass. ran it.")
|
|
26146
|
+
write_gate_fixture(repo, tasks=task)
|
|
26147
|
+
started = run_keel(
|
|
26148
|
+
repo, "gate", "task-start", "--change", "demo", "--task", "1.1",
|
|
26149
|
+
"--json", "--no-guard",
|
|
26150
|
+
)
|
|
26151
|
+
value = json.loads(started.stdout)["contract"]["fingerprint"]["value"]
|
|
26152
|
+
tasks_path = repo / "openspec/changes/demo/tasks.md"
|
|
26153
|
+
tasks_path.write_text(
|
|
26154
|
+
tasks_path.read_text(encoding="utf-8").replace(
|
|
26155
|
+
" - Contract: pending",
|
|
26156
|
+
f" - Contract: keel-task-capsule/v1 sha256:{value}",
|
|
26157
|
+
),
|
|
26158
|
+
encoding="utf-8",
|
|
26159
|
+
)
|
|
26160
|
+
return repo
|
|
26161
|
+
|
|
26162
|
+
def context(repo: Path):
|
|
26163
|
+
text = run_keel(repo, "context", "--change", "demo").stdout
|
|
26164
|
+
payload = json.loads(
|
|
26165
|
+
run_keel(repo, "context", "--change", "demo", "--json").stdout
|
|
26166
|
+
)
|
|
26167
|
+
return text, payload
|
|
26168
|
+
|
|
26169
|
+
with tempfile.TemporaryDirectory(prefix="keel-next-command-") as raw:
|
|
26170
|
+
root = Path(raw)
|
|
26171
|
+
|
|
26172
|
+
pending = repo_with(root, "pending", checked=False, evidence=False)
|
|
26173
|
+
text, payload = context(pending)
|
|
26174
|
+
command = (payload.get("nextAction") or {}).get("command")
|
|
26175
|
+
if not command:
|
|
26176
|
+
report(
|
|
26177
|
+
f"{label}: the next action carries no command; "
|
|
26178
|
+
f"{payload.get('nextAction')!r}."
|
|
26179
|
+
)
|
|
26180
|
+
return 1
|
|
26181
|
+
for needed in ("task-start", "--change demo", "--task 1.1"):
|
|
26182
|
+
if needed not in command:
|
|
26183
|
+
report(
|
|
26184
|
+
f"{label}: the reported command omits {needed!r}; "
|
|
26185
|
+
f"{command!r}."
|
|
26186
|
+
)
|
|
26187
|
+
return 1
|
|
26188
|
+
if command not in text:
|
|
26189
|
+
report(
|
|
26190
|
+
f"{label}: the text surface does not carry the same command as "
|
|
26191
|
+
f"--json; {command!r} not in {text!r}."
|
|
26192
|
+
)
|
|
26193
|
+
return 1
|
|
26194
|
+
|
|
26195
|
+
done = repo_with(root, "done", checked=False, evidence=True)
|
|
26196
|
+
_, payload = context(done)
|
|
26197
|
+
if "task-complete" not in str((payload.get("nextAction") or {}).get("command")):
|
|
26198
|
+
report(
|
|
26199
|
+
f"{label}: a task with completion evidence did not report the "
|
|
26200
|
+
f"task-complete invocation; {payload.get('nextAction')!r}."
|
|
26201
|
+
)
|
|
26202
|
+
return 1
|
|
26203
|
+
|
|
26204
|
+
closing = repo_with(root, "closing", checked=True, evidence=True)
|
|
26205
|
+
_, payload = context(closing)
|
|
26206
|
+
close_command = str((payload.get("nextAction") or {}).get("command") or "")
|
|
26207
|
+
if "change-close" not in close_command:
|
|
26208
|
+
report(
|
|
26209
|
+
f"{label}: a change whose tasks are complete did not report the "
|
|
26210
|
+
f"change-close invocation; {payload.get('nextAction')!r}."
|
|
26211
|
+
)
|
|
26212
|
+
return 1
|
|
26213
|
+
if "--action" not in close_command:
|
|
26214
|
+
report(
|
|
26215
|
+
f"{label}: the change-close command omits the argument that "
|
|
26216
|
+
f"stage requires; {close_command!r}."
|
|
26217
|
+
)
|
|
26218
|
+
return 1
|
|
26219
|
+
# Running exactly what was printed must not hit the input error the
|
|
26220
|
+
# report describes.
|
|
26221
|
+
printed = close_command.split()
|
|
26222
|
+
if printed and printed[0] == "keel":
|
|
26223
|
+
printed = printed[1:]
|
|
26224
|
+
ran = run_keel(closing, *printed)
|
|
26225
|
+
if "requires --action" in (ran.stdout + ran.stderr):
|
|
26226
|
+
report(
|
|
26227
|
+
f"{label}: the reported command still fails on the argument it "
|
|
26228
|
+
f"was supposed to supply; {(ran.stdout + ran.stderr)[:200]!r}."
|
|
26229
|
+
)
|
|
26230
|
+
return 1
|
|
26231
|
+
|
|
26232
|
+
idle = root / "idle"
|
|
26233
|
+
write_text(idle / "README.md", "# fixture\n")
|
|
26234
|
+
payload = json.loads(run_keel(idle, "context", "--json").stdout)
|
|
26235
|
+
if (payload.get("nextAction") or {}).get("command"):
|
|
26236
|
+
report(
|
|
26237
|
+
f"{label}: an action with nothing to run reported a command; "
|
|
26238
|
+
f"{payload.get('nextAction')!r}."
|
|
26239
|
+
)
|
|
26240
|
+
return 1
|
|
26241
|
+
|
|
26242
|
+
report(f"{label} scenario passed.")
|
|
26243
|
+
return 0
|
|
26244
|
+
|
|
26245
|
+
|
|
26246
|
+
# `keel --init` writes OpenSpec's command and skill surfaces, whose text says
|
|
26247
|
+
# `openspec new change "<name>"`. With only the Keel package installed globally
|
|
26248
|
+
# a bare `openspec` is not on PATH, which `keel --doctor` reports and names the
|
|
26249
|
+
# working invocation for. Those files belong to OpenSpec and Keel appends a
|
|
26250
|
+
# block rather than editing their body — so the block is where Keel says it.
|
|
26251
|
+
def validate_overlay_names_the_invocation_scenario() -> int:
|
|
26252
|
+
label = "the-overlay-names-the-invocation"
|
|
26253
|
+
start = "<!-- keel:openspec-surface-overlay"
|
|
26254
|
+
end = "<!-- keel:openspec-surface-overlay:end -->"
|
|
26255
|
+
with tempfile.TemporaryDirectory(prefix="keel-overlay-invocation-") as raw:
|
|
26256
|
+
repo = Path(raw)
|
|
26257
|
+
# The surfaces OpenSpec writes, as OpenSpec writes them: the overlay is
|
|
26258
|
+
# merged into files that already exist, and their body is the text this
|
|
26259
|
+
# scenario asserts Keel does not touch.
|
|
26260
|
+
upstream = 'Run `openspec new change "<name>"` to begin.\n'
|
|
26261
|
+
for action in ("propose", "apply", "sync", "archive"):
|
|
26262
|
+
write_text(repo / f".claude/commands/opsx/{action}.md", upstream)
|
|
26263
|
+
for skill in (
|
|
26264
|
+
"openspec-propose",
|
|
26265
|
+
"openspec-apply-change",
|
|
26266
|
+
"openspec-sync-specs",
|
|
26267
|
+
"openspec-archive-change",
|
|
26268
|
+
):
|
|
26269
|
+
write_text(repo / f".claude/skills/{skill}/SKILL.md", upstream)
|
|
26270
|
+
installed = run_keel(repo, "--install", "--target", "claude")
|
|
26271
|
+
if installed.returncode != 0:
|
|
26272
|
+
report(
|
|
26273
|
+
f"{label}: the fixture install failed; "
|
|
26274
|
+
f"{(installed.stderr or installed.stdout).strip()[:300]}"
|
|
26275
|
+
)
|
|
26276
|
+
return 1
|
|
26277
|
+
|
|
26278
|
+
carrying = [
|
|
26279
|
+
path
|
|
26280
|
+
for path in sorted(repo.rglob("*.md"))
|
|
26281
|
+
if start in path.read_text(encoding="utf-8", errors="replace")
|
|
26282
|
+
]
|
|
26283
|
+
if not carrying:
|
|
26284
|
+
report(f"{label}: the install wrote no file carrying an overlay.")
|
|
26285
|
+
return 1
|
|
26286
|
+
for path in carrying:
|
|
26287
|
+
content = path.read_text(encoding="utf-8")
|
|
26288
|
+
block = content[content.index(start):content.index(end) + len(end)]
|
|
26289
|
+
for needed in ("keel openspec", "keel --doctor"):
|
|
26290
|
+
if needed not in block:
|
|
26291
|
+
report(
|
|
26292
|
+
f"{label}: the overlay in {path.name} does not name "
|
|
26293
|
+
f"{needed!r}."
|
|
26294
|
+
)
|
|
26295
|
+
return 1
|
|
26296
|
+
# The boundary: everything outside the block is OpenSpec's.
|
|
26297
|
+
outside = (
|
|
26298
|
+
content[: content.index(start)]
|
|
26299
|
+
+ content[content.index(end) + len(end):]
|
|
26300
|
+
)
|
|
26301
|
+
if start in outside or "keel openspec" in outside:
|
|
26302
|
+
report(
|
|
26303
|
+
f"{label}: {path.name} carries Keel invocation text outside "
|
|
26304
|
+
"the overlay block, so the OpenSpec-authored body was "
|
|
26305
|
+
"edited."
|
|
26306
|
+
)
|
|
26307
|
+
return 1
|
|
26308
|
+
|
|
26309
|
+
report(f"{label} scenario passed.")
|
|
26310
|
+
return 0
|
|
26311
|
+
|
|
26312
|
+
|
|
26122
26313
|
# A scenario name, as the registry spells one. Two registered names carry no
|
|
26123
26314
|
# hyphen — `cli` and `uninstall` — so requiring one would leave exactly those
|
|
26124
26315
|
# two unchecked, and allowing single words was measured to add no false
|
|
@@ -26359,6 +26550,8 @@ SCENARIOS: tuple = (
|
|
|
26359
26550
|
("an-explanation-is-printed-once", validate_explanation_is_printed_once_scenario),
|
|
26360
26551
|
("a-paused-change-is-not-the-next-action", validate_paused_change_is_not_the_next_action_scenario),
|
|
26361
26552
|
("evidence-survives-what-did-not-change", validate_evidence_survives_what_did_not_change_scenario),
|
|
26553
|
+
("the-next-action-is-a-command", validate_next_action_is_a_command_scenario),
|
|
26554
|
+
("the-overlay-names-the-invocation", validate_overlay_names_the_invocation_scenario),
|
|
26362
26555
|
(
|
|
26363
26556
|
"authored-scenario-names-are-registered",
|
|
26364
26557
|
validate_authored_scenario_names_scenario,
|
package/src/core/context.js
CHANGED
|
@@ -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}`
|