@christang/keel 5.39.0 → 5.46.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 +12 -2
- package/assets/bootstrap/AGENTS.md +1 -1
- package/package.json +4 -2
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/plugins/keel/skills/keel-run-single-task-goal/SKILL.md +1 -1
- package/scripts/install_to_repo.py +74 -4
- package/scripts/validate_plugin.py +1775 -117
- package/src/core/config.js +7 -1
- package/src/core/gates.js +138 -26
- package/src/core/task-contract.js +24 -5
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ because a permission granted in conversation does not survive a context reset. D
|
|
|
132
132
|
`keel/config.yaml` instead:
|
|
133
133
|
|
|
134
134
|
```yaml
|
|
135
|
-
authorize: # accepted names: commit, push, release, archive
|
|
135
|
+
authorize: # accepted names: commit, push, release, archive, continuation
|
|
136
136
|
- commit
|
|
137
137
|
- push
|
|
138
138
|
```
|
|
@@ -141,6 +141,16 @@ A task that authors no `Autonomy boundary:` inherits the declaration, and the co
|
|
|
141
141
|
names `keel/config.yaml` as that entry's source so an inherited authorization is never mistaken
|
|
142
142
|
for one the task decided. A task that authors its own boundary keeps it.
|
|
143
143
|
|
|
144
|
+
`continuation`, the fifth name, is the between-task one: it covers exactly the boundary between a
|
|
145
|
+
durably complete task and the next unchecked task of the same change, inside a change whose
|
|
146
|
+
`tasks.md` you approved — the stop that re-asks for an approval already given. It removes only that
|
|
147
|
+
confirmation: the next task still starts through `keel gate task-start` with its own recorded
|
|
148
|
+
fingerprint, every gate, evidence requirement, semantic Review, and the write guard run unchanged,
|
|
149
|
+
and a stop with its own trigger — a blocker, fingerprint drift, an out-of-scope need, an unresolved
|
|
150
|
+
question — still stops. It authorizes no repository action and schedules nothing. On an older Keel
|
|
151
|
+
whose vocabulary predates the word, the entry is unrecognized and the whole declaration authorizes
|
|
152
|
+
nothing until corrected — fail-closed, never a silent grant.
|
|
153
|
+
|
|
144
154
|
Three things the declaration is not:
|
|
145
155
|
|
|
146
156
|
- **Not a way past a gate.** It authorizes the action, never the proof. `keel gate task-complete`
|
|
@@ -148,7 +158,7 @@ Three things the declaration is not:
|
|
|
148
158
|
anything.
|
|
149
159
|
- **Not a trigger.** It removes a confirmation, not the step that reaches the action. Nothing
|
|
150
160
|
schedules itself, and no next task is selected for you.
|
|
151
|
-
- **Not open-ended.** The
|
|
161
|
+
- **Not open-ended.** The five names above are the whole vocabulary. An unrecognized entry is
|
|
152
162
|
reported with the accepted names and the declaration authorizes nothing until you fix it — a
|
|
153
163
|
typo never becomes a silent grant.
|
|
154
164
|
|
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.
|
|
5
|
+
"version": "5.46.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"src/core/",
|
|
18
18
|
"assets/",
|
|
19
19
|
"README.md",
|
|
20
|
-
"plugins/"
|
|
20
|
+
"plugins/",
|
|
21
|
+
"!**/__pycache__",
|
|
22
|
+
"!**/*.pyc"
|
|
21
23
|
],
|
|
22
24
|
"scripts": {
|
|
23
25
|
"validate": "node scripts/run_python.js scripts/validate_plugin.py",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.46.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.46.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",
|
|
@@ -50,7 +50,7 @@ If any of these hold, stop and use the normal manual Keel loop.
|
|
|
50
50
|
4. Surface every command result and gate outcome in the transcript before any success claim.
|
|
51
51
|
5. Pass the current agent's Review, then `keel gate task-complete`.
|
|
52
52
|
6. The current agent durably checks the task checkbox.
|
|
53
|
-
7. Stop. Continuing to another task requires a new explicit user instruction and a new start fingerprint; there is no hidden scheduler or automatic next-task selection.
|
|
53
|
+
7. Stop. Continuing to another task requires a new explicit user instruction and a new start fingerprint; a standing `continuation` authorization in `keel/config.yaml` is the durable form of that instruction, covering only the next unchecked task of the same change in its approved `tasks.md` order. Each next task still passes `keel gate task-start` with its own recorded fingerprint, and there is still no hidden scheduler or automatic next-task selection — the declaration removes the confirmation, never the selection, the gates, or the evidence.
|
|
54
54
|
|
|
55
55
|
Resume reconstructs the goal from OpenSpec and Git only. Fingerprint drift, checkout divergence, a completed authorization, or missing authorization hard-stops before any product write; pass `--expected-fingerprint` and `--expected-owner` to assert the recorded authorization.
|
|
56
56
|
|
|
@@ -112,7 +112,7 @@ TASKS_COMMIT_STATUS_PATTERNS = (
|
|
|
112
112
|
"remove dirty/uncommitted state from tasks.md; keep durable work state in OpenSpec and use HANDOFF only as an explicit pointer override",
|
|
113
113
|
),
|
|
114
114
|
(
|
|
115
|
-
re.compile(r"(
|
|
115
|
+
re.compile(r"(?:未合入|待合入|已合入|合入\s*(?:master|main))"),
|
|
116
116
|
"remove commit or merge state from tasks.md; git log is the source of truth",
|
|
117
117
|
),
|
|
118
118
|
(
|
|
@@ -120,6 +120,28 @@ TASKS_COMMIT_STATUS_PATTERNS = (
|
|
|
120
120
|
"remove branch merge state from tasks.md; git log is the source of truth",
|
|
121
121
|
),
|
|
122
122
|
)
|
|
123
|
+
# 合入 names the git act and has no other reading, so it is refused on its own
|
|
124
|
+
# above. 提交 is an ordinary transitive verb — 提交资料, 提交审核, 提交申请 — and
|
|
125
|
+
# matching it bare refused an Evidence line about a third-party review queue
|
|
126
|
+
# while accepting the same sentence in English (#103). A check whose verdict
|
|
127
|
+
# depends on which language the author wrote in is not checking what it claims
|
|
128
|
+
# to, so this family is refused only where the line says the subject is git.
|
|
129
|
+
TASKS_SUBMISSION_STATE_RE = re.compile(r"(?:未提交|待提交|已提交|尚未提交)")
|
|
130
|
+
# The words that name git or a git object. Deliberately not `_HASH_CONTEXT_WORD`:
|
|
131
|
+
# that list holds 提交 and 合入 themselves, because its job is to let a state word
|
|
132
|
+
# supply context to a hash-shaped token, and reusing it here would let 已提交
|
|
133
|
+
# supply its own context and change nothing. The ASCII words carry boundaries for
|
|
134
|
+
# the reason #65 established; the Chinese ones cannot and do not.
|
|
135
|
+
TASKS_GIT_CONTEXT_RE = re.compile(
|
|
136
|
+
r"(?i)\b(?:git|commits?|committed|committing|master|main|HEAD|branch(?:es)?"
|
|
137
|
+
r"|PR|merges?|merged)\b"
|
|
138
|
+
r"|分支|仓库|代码库|代码|合入|工作区|暂存"
|
|
139
|
+
)
|
|
140
|
+
TASKS_SUBMISSION_MESSAGE = (
|
|
141
|
+
"remove commit or merge state from tasks.md; git log is the source of truth"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
123
145
|
# A commit identifier is hexadecimal, and `a`-`f` are the only reason those
|
|
124
146
|
# letters appear in one. A run of decimal digits alone is a phone number, a
|
|
125
147
|
# timestamp, an order number, or a port — evidence prose, not state git owns.
|
|
@@ -137,8 +159,10 @@ _HASH_SHAPED_TOKEN = r"\b(?=[0-9a-f]{7,40}\b)[0-9a-f]*[a-f][0-9a-f]*\b"
|
|
|
137
159
|
# the words an author writes about the act this rule exists to catch, and the
|
|
138
160
|
# stricter list that drops `committed` and `hashes` would cost real refusals.
|
|
139
161
|
# The Chinese words carry no boundary because none is definable between two
|
|
140
|
-
# word characters — `\b提交\b` matches neither `已提交` nor
|
|
141
|
-
#
|
|
162
|
+
# word characters — `\b提交\b` matches neither `已提交` nor `未提交`. What that
|
|
163
|
+
# buys is this rule: a state word beside a hash-shaped token still supplies the
|
|
164
|
+
# context that makes the token an identifier. The wording rule above no longer
|
|
165
|
+
# rests on it, because 提交 needs a git word of its own (#103).
|
|
142
166
|
_HASH_CONTEXT_WORD = (
|
|
143
167
|
r"(?:\b(?:commits?|committed|committing|master|main|HEAD|hash(?:es)?)\b"
|
|
144
168
|
r"|提交|合入|哈希)"
|
|
@@ -704,6 +728,37 @@ def covers_field_lines(content: str) -> set[int]:
|
|
|
704
728
|
return lines
|
|
705
729
|
|
|
706
730
|
|
|
731
|
+
# What an author quotes is content they cite, not a claim they make. Evidence
|
|
732
|
+
# prose quotes the command that ran, the output it printed, the branch base it
|
|
733
|
+
# ran against, and the name of the requirement under change — and every one of
|
|
734
|
+
# those carries exactly the words these rules refuse. `withoutInlineCode()` in
|
|
735
|
+
# `src/core/task-contract.js` settled this shape for the field reader; the two
|
|
736
|
+
# reports that reached this reader (#65 items 2 and 4) are the same shape
|
|
737
|
+
# arriving late.
|
|
738
|
+
#
|
|
739
|
+
# The ASCII single quote is deliberately absent. It is an apostrophe far more
|
|
740
|
+
# often than a quotation mark, and a span opened by `doesn't` would silence the
|
|
741
|
+
# remainder of the line — a rule that stops running without saying so, which is
|
|
742
|
+
# the failure no fixture catches by accident.
|
|
743
|
+
QUOTED_SPAN_RE = re.compile(
|
|
744
|
+
r"`[^`\n]*`"
|
|
745
|
+
r"|\"[^\"\n]*\""
|
|
746
|
+
r"|\u201c[^\u201d\n]*\u201d"
|
|
747
|
+
r"|\u300c[^\u300d\n]*\u300d"
|
|
748
|
+
r"|\u300a[^\u300b\n]*\u300b"
|
|
749
|
+
)
|
|
750
|
+
FENCE_RE = re.compile(r"^\s*(?:```|~~~)")
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def without_quoted_spans(line: str) -> str:
|
|
754
|
+
"""Blank out the quoted spans of a line, leaving its length reading intact.
|
|
755
|
+
|
|
756
|
+
The replacement is a space rather than nothing so two tokens either side of
|
|
757
|
+
a span cannot be joined into a third word that neither of them was.
|
|
758
|
+
"""
|
|
759
|
+
return QUOTED_SPAN_RE.sub(" ", line)
|
|
760
|
+
|
|
761
|
+
|
|
707
762
|
def check_tasks_semantics(repo: Path) -> list[str]:
|
|
708
763
|
changes_root = repo / OPENSPEC_ROOT / "changes"
|
|
709
764
|
if not changes_root.is_dir():
|
|
@@ -725,12 +780,27 @@ def check_tasks_semantics(repo: Path) -> list[str]:
|
|
|
725
780
|
errors.append(f"{relative}:{line}: {message}")
|
|
726
781
|
|
|
727
782
|
cited = covers_field_lines(content)
|
|
783
|
+
fenced = False
|
|
728
784
|
for line_number, line in enumerate(content.splitlines(), start=1):
|
|
729
|
-
|
|
785
|
+
# A fenced block is one long quoted span, and its own delimiter
|
|
786
|
+
# lines carry no prose. Tracking the state here rather than
|
|
787
|
+
# pre-stripping the file keeps every reported line number the
|
|
788
|
+
# line number the author sees.
|
|
789
|
+
if FENCE_RE.match(line):
|
|
790
|
+
fenced = not fenced
|
|
730
791
|
continue
|
|
792
|
+
if fenced or is_tasks_rule_line(line) or line_number in cited:
|
|
793
|
+
continue
|
|
794
|
+
line = without_quoted_spans(line)
|
|
731
795
|
for pattern, message in TASKS_COMMIT_STATUS_PATTERNS:
|
|
732
796
|
if pattern.search(line):
|
|
733
797
|
errors.append(f"{relative}:{line_number}: {message}")
|
|
798
|
+
if TASKS_SUBMISSION_STATE_RE.search(line) and TASKS_GIT_CONTEXT_RE.search(
|
|
799
|
+
line
|
|
800
|
+
):
|
|
801
|
+
errors.append(
|
|
802
|
+
f"{relative}:{line_number}: {TASKS_SUBMISSION_MESSAGE}"
|
|
803
|
+
)
|
|
734
804
|
if TASKS_CONTEXTUAL_HASH_RE.search(line):
|
|
735
805
|
errors.append(
|
|
736
806
|
f"{relative}:{line_number}: remove contextual commit hash from tasks.md; git log is the source of truth"
|