@christang/keel 5.72.0 → 5.73.1
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 +2 -2
- package/assets/bootstrap/AGENTS.md +1 -1
- package/package.json +1 -1
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/scripts/bump_version.js +10 -4
- package/scripts/validate_plugin.py +109 -2
- package/src/core/gates.js +7 -5
- package/src/core/task-contract.js +19 -6
package/README.md
CHANGED
|
@@ -287,8 +287,8 @@ because the only thing the protocol says about merging is that the agent may not
|
|
|
287
287
|
|
|
288
288
|
`merge: human` says the opposite. A bare `repository` is refused — "nobody reviews this" is only honest
|
|
289
289
|
beside what replaced the reviewer. It is not a permission: `authorize:` has no `merge` entry and should
|
|
290
|
-
not gain one. Keel reads the declaration and never GitHub, so it cannot check that
|
|
291
|
-
|
|
290
|
+
not gain one. Keel reads the declaration and never GitHub, so it cannot check that your repository really
|
|
291
|
+
merges that way; it reports what you declared.
|
|
292
292
|
|
|
293
293
|
### Full vs Lite
|
|
294
294
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.73.1",
|
|
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.73.1",
|
|
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",
|
package/scripts/bump_version.js
CHANGED
|
@@ -165,13 +165,19 @@ function main() {
|
|
|
165
165
|
|
|
166
166
|
prependChangelogEntry(newVersion);
|
|
167
167
|
|
|
168
|
+
// A version is released by landing its pull request: publish.yml's `land`
|
|
169
|
+
// job merges it once full-gate passes, then publishes, tags, and releases in
|
|
170
|
+
// the same run. It skips a version whose tag already exists, so a tag pushed
|
|
171
|
+
// by hand with no release means nothing publishes it at all (#162).
|
|
168
172
|
process.stdout.write(
|
|
169
173
|
`\nDone. Next:\n` +
|
|
170
|
-
` 1.
|
|
174
|
+
` 1. Write the ${newVersion} entry into its stub in keel/CHANGELOG.md.\n` +
|
|
171
175
|
` 2. npm test\n` +
|
|
172
|
-
` 3. git
|
|
173
|
-
`
|
|
174
|
-
`
|
|
176
|
+
` 3. Commit on a branch (git add -A, so an archived change is included),\n` +
|
|
177
|
+
` push it, and open a pull request into main.\n` +
|
|
178
|
+
` When full-gate passes, the land job merges it, publishes ${newVersion} to npm,\n` +
|
|
179
|
+
` and creates tag v${newVersion} with its release. Do not tag by hand: the job\n` +
|
|
180
|
+
` skips a version whose tag exists, and a pushed tag alone publishes nothing.\n`
|
|
175
181
|
);
|
|
176
182
|
}
|
|
177
183
|
|
|
@@ -38,8 +38,8 @@ REQUIRED_SCRIPTS = [
|
|
|
38
38
|
"scripts/validate_plugin.py",
|
|
39
39
|
]
|
|
40
40
|
|
|
41
|
-
PACKAGE_VERSION = "5.
|
|
42
|
-
PROTOCOL_VERSION = "5.
|
|
41
|
+
PACKAGE_VERSION = "5.73.1"
|
|
42
|
+
PROTOCOL_VERSION = "5.73.1"
|
|
43
43
|
LEGACY_MANAGED_START = "<!-- keel:start version=2.1 -->"
|
|
44
44
|
OPENSPEC_SCHEMA_NAME = "keel-spec-driven"
|
|
45
45
|
# Mirrors KEEL_PACKAGE_NAME in scripts/install_to_repo.py, one of the two
|
|
@@ -5031,6 +5031,71 @@ def validate_section_boundary_scenario() -> int:
|
|
|
5031
5031
|
report(repr(problems_of(unstarted["tail"])))
|
|
5032
5032
|
return 1
|
|
5033
5033
|
|
|
5034
|
+
# Cells 5 and 6: the heading half of the boundary, in the tail, where
|
|
5035
|
+
# it is the only half that applies. An indented `##` line inside the
|
|
5036
|
+
# section's own body is not a heading, and the entry after it must still
|
|
5037
|
+
# be judged. The tolerant spelling `parseTasks()` uses for task bodies
|
|
5038
|
+
# ends the section there and drops that entry without a word — issue
|
|
5039
|
+
# #160, measured on the abandoned first attempt at #71 as `7bd6448`.
|
|
5040
|
+
# Both readers are covered because each slices its own section.
|
|
5041
|
+
indented = " ## an indented line is not a heading\n"
|
|
5042
|
+
indented_invalidates = (
|
|
5043
|
+
"## Invalidates\n\n"
|
|
5044
|
+
'- I1: "the wording that is now wrong" — README.md. Updated by: 1.1\n'
|
|
5045
|
+
+ indented
|
|
5046
|
+
+ '- I2: "wording nothing updates" — README.md.\n'
|
|
5047
|
+
)
|
|
5048
|
+
payload = start(
|
|
5049
|
+
"invalidates-indented-tail",
|
|
5050
|
+
section_boundary_tasks_md(
|
|
5051
|
+
section=indented_invalidates, task=start_task, position="tail"
|
|
5052
|
+
),
|
|
5053
|
+
)
|
|
5054
|
+
if not readable(payload, "invalidates-indented-tail", "task-start"):
|
|
5055
|
+
return 1
|
|
5056
|
+
if not [
|
|
5057
|
+
message
|
|
5058
|
+
for message in closure_problems(payload, "invalidation-closure")
|
|
5059
|
+
if "I2" in message
|
|
5060
|
+
]:
|
|
5061
|
+
report(
|
|
5062
|
+
f"{label} accepted an invalidation that closes nothing because "
|
|
5063
|
+
"it follows an indented `##` line; the section ended at a line "
|
|
5064
|
+
"that is not a heading."
|
|
5065
|
+
)
|
|
5066
|
+
report(repr(payload.get("problems")))
|
|
5067
|
+
return 1
|
|
5068
|
+
|
|
5069
|
+
indented_coverage = (
|
|
5070
|
+
"## Invalidates\n\n- None.\n\n"
|
|
5071
|
+
"## Expectation Coverage\n\n"
|
|
5072
|
+
"- E1: The task owns its file. Covered by: 1.1\n"
|
|
5073
|
+
+ indented
|
|
5074
|
+
+ "- E3: Nothing closes this one.\n"
|
|
5075
|
+
)
|
|
5076
|
+
payload = close(
|
|
5077
|
+
"coverage-indented-tail",
|
|
5078
|
+
section_boundary_tasks_md(
|
|
5079
|
+
section=indented_coverage,
|
|
5080
|
+
task=section_boundary_task("1.1", checked=True),
|
|
5081
|
+
position="tail",
|
|
5082
|
+
),
|
|
5083
|
+
)
|
|
5084
|
+
if not readable(payload, "coverage-indented-tail", "change-close"):
|
|
5085
|
+
return 1
|
|
5086
|
+
if not [
|
|
5087
|
+
message
|
|
5088
|
+
for message in closure_problems(payload, "expectation-closure")
|
|
5089
|
+
if "E3" in message
|
|
5090
|
+
]:
|
|
5091
|
+
report(
|
|
5092
|
+
f"{label} accepted an expectation that closes nothing because "
|
|
5093
|
+
"it follows an indented `##` line; the section ended at a line "
|
|
5094
|
+
"that is not a heading."
|
|
5095
|
+
)
|
|
5096
|
+
report(repr(payload.get("problems")))
|
|
5097
|
+
return 1
|
|
5098
|
+
|
|
5034
5099
|
report(f"{label} scenario passed.")
|
|
5035
5100
|
return 0
|
|
5036
5101
|
|
|
@@ -10016,6 +10081,48 @@ def validate_task_body_ends_at_heading_scenario() -> int:
|
|
|
10016
10081
|
"preceding task's fields."
|
|
10017
10082
|
)
|
|
10018
10083
|
return 1
|
|
10084
|
+
# An indented `##` line is not a heading — issue #160. The tolerant
|
|
10085
|
+
# test ended the task there, and a field declared after it with a
|
|
10086
|
+
# documented default was replaced by that default without a word: the
|
|
10087
|
+
# stop rule below vanished and task-start still passed. The fields are
|
|
10088
|
+
# ordered so the dropped one is exactly such a field.
|
|
10089
|
+
write_text(
|
|
10090
|
+
repo / "openspec/changes/indented/tasks.md",
|
|
10091
|
+
header
|
|
10092
|
+
+ "- [ ] 1.1 Exercise task contract\n"
|
|
10093
|
+
" - Covers:\n"
|
|
10094
|
+
" - E1: Public behavior passes.\n"
|
|
10095
|
+
" - Touch:\n"
|
|
10096
|
+
" - src/feature.js\n"
|
|
10097
|
+
" - Verify:\n"
|
|
10098
|
+
" - Strategy: evidence-first\n"
|
|
10099
|
+
" - Reason: this is a gate fixture; it exercises contract structure and has no executable behavior that can fail first\n"
|
|
10100
|
+
" - M1: node test.js asserts the recorded feed status\n"
|
|
10101
|
+
" - Evidence:\n"
|
|
10102
|
+
" - Contract: pending\n"
|
|
10103
|
+
" - M1: pending\n"
|
|
10104
|
+
" - Acceptance:\n"
|
|
10105
|
+
" - Public behavior passes.\n"
|
|
10106
|
+
" ## an indented line is not a heading\n"
|
|
10107
|
+
" - Stop Rules:\n"
|
|
10108
|
+
" - Stop on the rule declared after the indented line.\n"
|
|
10109
|
+
"\n## Invalidates\n\n- None.\n",
|
|
10110
|
+
)
|
|
10111
|
+
indented = json.loads(
|
|
10112
|
+
run_keel(
|
|
10113
|
+
repo, "gate", "task-start", "--change", "indented", "--task",
|
|
10114
|
+
"1.1", "--no-guard", "--json",
|
|
10115
|
+
).stdout
|
|
10116
|
+
)
|
|
10117
|
+
if "Stop on the rule declared after the indented line." not in json.dumps(
|
|
10118
|
+
indented
|
|
10119
|
+
):
|
|
10120
|
+
report(
|
|
10121
|
+
"task-body-ends-at-heading: task-start dropped the fields "
|
|
10122
|
+
"declared after an indented `##` line; the task ended at a line "
|
|
10123
|
+
f"that is not a heading (status {indented.get('status')!r})."
|
|
10124
|
+
)
|
|
10125
|
+
return 1
|
|
10019
10126
|
# --record must anchor the last task's own Contract line, not the stray
|
|
10020
10127
|
# one planted in the trailing section.
|
|
10021
10128
|
recorded = run_keel(
|
package/src/core/gates.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
declaredCommandLabels,
|
|
14
14
|
field,
|
|
15
15
|
isConcrete,
|
|
16
|
+
isHeadingLine,
|
|
16
17
|
isPassingReviewStatus,
|
|
17
18
|
parseTasks,
|
|
18
19
|
unfilledToken,
|
|
@@ -1597,10 +1598,11 @@ function taskComplete(repo, options) {
|
|
|
1597
1598
|
//
|
|
1598
1599
|
// The task half is the task list already parsed for this file rather than a
|
|
1599
1600
|
// second checkbox pattern, so it cannot drift from the boundary `parseTasks()`
|
|
1600
|
-
// applies to a task's own body. The heading half
|
|
1601
|
-
//
|
|
1602
|
-
// section at an indented `##` line inside
|
|
1603
|
-
//
|
|
1601
|
+
// applies to a task's own body. The heading half is `isHeadingLine()`, the same
|
|
1602
|
+
// test `parseTasks()` uses. It must stay the column-zero one: the tolerant
|
|
1603
|
+
// spelling truncates a tail-position section at an indented `##` line inside
|
|
1604
|
+
// its own body and drops every entry after it, which is this same defect
|
|
1605
|
+
// pointed the other way (#160).
|
|
1604
1606
|
function sectionBody(content, headingOffset, tasks) {
|
|
1605
1607
|
const lines = content.split(/\r?\n/);
|
|
1606
1608
|
const headingLine = content.slice(0, headingOffset).split(/\r?\n/).length - 1;
|
|
@@ -1609,7 +1611,7 @@ function sectionBody(content, headingOffset, tasks) {
|
|
|
1609
1611
|
if (task.line > headingLine && task.line < end) end = task.line;
|
|
1610
1612
|
}
|
|
1611
1613
|
for (let cursor = headingLine + 1; cursor < end; cursor += 1) {
|
|
1612
|
-
if (
|
|
1614
|
+
if (isHeadingLine(lines[cursor])) {
|
|
1613
1615
|
end = cursor;
|
|
1614
1616
|
break;
|
|
1615
1617
|
}
|
|
@@ -59,6 +59,17 @@ function unfilledToken(value) {
|
|
|
59
59
|
return match ? match[0] : null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// A heading is a `##` line at column zero. An indented one is text: inside a
|
|
63
|
+
// task it belongs to the field that is open, inside a change-level section it
|
|
64
|
+
// is not an entry. Both readers decide through this one test — issue #160.
|
|
65
|
+
// The tolerant spelling `/^\s*##\s/` ended a task at an indented line and
|
|
66
|
+
// dropped every field after it, and a field with a documented default was
|
|
67
|
+
// replaced by that default without a word; in a section it dropped the entries
|
|
68
|
+
// after the line with no refusal at all.
|
|
69
|
+
function isHeadingLine(line) {
|
|
70
|
+
return /^##\s/.test(line);
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
function parseTasks(content) {
|
|
63
74
|
const lines = content.split(/\r?\n/);
|
|
64
75
|
const tasks = [];
|
|
@@ -75,16 +86,17 @@ function parseTasks(content) {
|
|
|
75
86
|
});
|
|
76
87
|
}
|
|
77
88
|
for (let index = 0; index < tasks.length; index += 1) {
|
|
78
|
-
// A task body ends at the next task or the next `##` heading
|
|
79
|
-
// comes first. Without the heading bound a
|
|
80
|
-
// `## Invalidates` was appended to whichever
|
|
81
|
-
// Evidence, in every shipped template — so a
|
|
82
|
-
// Evidence non-concrete and the gate blamed a
|
|
89
|
+
// A task body ends at the next task or the next `##` heading (see
|
|
90
|
+
// `isHeadingLine`), whichever comes first. Without the heading bound a
|
|
91
|
+
// change-level section such as `## Invalidates` was appended to whichever
|
|
92
|
+
// field was open last — the Evidence, in every shipped template — so a
|
|
93
|
+
// token quoted there made the Evidence non-concrete and the gate blamed a
|
|
94
|
+
// task that was fine.
|
|
83
95
|
const nextTask =
|
|
84
96
|
index + 1 < tasks.length ? tasks[index + 1].line : lines.length;
|
|
85
97
|
let end = nextTask;
|
|
86
98
|
for (let cursor = tasks[index].line + 1; cursor < nextTask; cursor += 1) {
|
|
87
|
-
if (
|
|
99
|
+
if (isHeadingLine(lines[cursor])) {
|
|
88
100
|
end = cursor;
|
|
89
101
|
break;
|
|
90
102
|
}
|
|
@@ -1538,6 +1550,7 @@ module.exports = {
|
|
|
1538
1550
|
isConcrete,
|
|
1539
1551
|
isPassingReviewStatus,
|
|
1540
1552
|
loadTaskContract,
|
|
1553
|
+
isHeadingLine,
|
|
1541
1554
|
parseTasks,
|
|
1542
1555
|
taskStartContractProblems,
|
|
1543
1556
|
unfilledToken,
|