@christang/keel 5.39.0 → 5.44.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
@@ -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 four names above are the whole vocabulary. An unrecognized entry is
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
 
@@ -1,4 +1,4 @@
1
- <!-- keel:start version=5.39.0 -->
1
+ <!-- keel:start version=5.44.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/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.39.0",
5
+ "version": "5.44.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.39.0",
3
+ "version": "5.44.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.39.0",
3
+ "version": "5.44.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"(?:未提交|待提交|已提交|尚未提交|未合入|待合入|已合入|合入\s*(?:master|main))"),
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 `未提交`, which are
141
- # exactly what the wording rule is for.
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
- if is_tasks_rule_line(line) or line_number in cited:
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"