@christang/keel 5.4.0 → 5.6.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
@@ -125,6 +125,73 @@ Set `KEEL_SESSION_PANEL=1` to draw it as a framed panel with the Keel mark inste
125
125
  default, and turning it on changes nothing but the presentation — the same status and the same
126
126
  next command are in both forms.
127
127
 
128
+ ### Standing authorization
129
+
130
+ Keel asks before a repository action it has no authority for, and it asks again next session,
131
+ because a permission granted in conversation does not survive a context reset. Declare it once in
132
+ `keel/config.yaml` instead:
133
+
134
+ ```yaml
135
+ authorize: # accepted names: commit, push, release, archive
136
+ - commit
137
+ - push
138
+ ```
139
+
140
+ A task that authors no `Autonomy boundary:` inherits the declaration, and the compiled capsule
141
+ names `keel/config.yaml` as that entry's source so an inherited authorization is never mistaken
142
+ for one the task decided. A task that authors its own boundary keeps it.
143
+
144
+ Three things the declaration is not:
145
+
146
+ - **Not a way past a gate.** It authorizes the action, never the proof. `keel gate task-complete`
147
+ returns exactly the same verdict, and the same failure text, whether or not you declared
148
+ anything.
149
+ - **Not a trigger.** It removes a confirmation, not the step that reaches the action. Nothing
150
+ 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
152
+ reported with the accepted names and the declaration authorizes nothing until you fix it — a
153
+ typo never becomes a silent grant.
154
+
155
+ The block is absent by default, and a repository that declares nothing behaves exactly as it did
156
+ before this feature existed. `keel --doctor` reports what is declared.
157
+
158
+ ### Decision precedents
159
+
160
+ A decision you make in conversation is spent at the next context reset, and the reasoning that
161
+ settled it — the part that would generalise to a decision you have not met yet — goes with it.
162
+ Point Keel at a directory of precedents and it consults them instead of asking you again:
163
+
164
+ ```yaml
165
+ precedents: ../my-decisions # any path; it may live outside this repository
166
+ ```
167
+
168
+ Keel ships no precedent and creates no store. It reads that directory and nothing else — it never
169
+ clones, pulls, or reaches the network — so one directory outside your repositories can serve all of
170
+ them, and a path that is not there behaves exactly as no declaration at all.
171
+
172
+ Each precedent is a markdown file carrying an `Applies when:` header, the materiality category it
173
+ belongs to, a status of `recorded` or `authorized`, the decision, and **the rationale**. The last is
174
+ load-bearing: *"chose A"* applies only to the situation literally recorded, while *"chose A because
175
+ B fails offline"* can be applied to a case nobody has seen — and recognised as not applying when the
176
+ new case is online. A precedent with no rationale is reported incomplete and is never applied.
177
+
178
+ Three rules govern how they are used:
179
+
180
+ - **A precedent is cited exactly where it replaced a question.** If applying it meant you were not
181
+ asked something you would have been asked, the reply names it. Routine decisions are not cited,
182
+ so a citation always marks a decision made in your place.
183
+ - **Only you promote one.** A precedent enters as `recorded` and is offered as a recommendation
184
+ while the question is still asked. It becomes `authorized` when you accept a promotion that was
185
+ proposed to you — never by a usage count, which would cross with nobody watching.
186
+ - **A precedent answers a recurrence; it never reclassifies.** It can shorten a decision inside its
187
+ category. It cannot move a decision out of the categories that require asking you, and no
188
+ accumulation of precedents makes a category stop mattering.
189
+
190
+ As with standing authorization, a precedent informs a decision and never substitutes for a proof:
191
+ gates, evidence, review, and the write guard are untouched by anything in the store. The session
192
+ start line reports the store's size and freshness only — precedent bodies load when a decision is
193
+ actually being made.
194
+
128
195
  ### Full vs Lite
129
196
 
130
197
  Use **Full mode** (the OpenSpec flow above) for new features, interface or protocol changes,
@@ -160,7 +227,8 @@ Keel splits verification into two layers so a slow suite never blocks your push:
160
227
  run at CI or at `keel gate change-close`.
161
228
 
162
229
  A task's `Verify` checks stay fast; the slow or exhaustive layer belongs to the full gate, not the
163
- local pre-push. Declare your fast check once in `keel/config.yaml`:
230
+ local pre-push. Declare your fast check in `keel/config.yaml`, the same file that holds your
231
+ standing authorization:
164
232
 
165
233
  ```yaml
166
234
  fast_check: npm test -- --fast # your project's seconds-scale check
@@ -179,7 +247,9 @@ is repo-local and reversible.
179
247
 
180
248
  ## Domain lenses
181
249
 
182
- Keel's core is pure process; it ships no domain knowledge of its own. Domain guidance lives in
250
+ Keel's core is pure process; it ships no domain knowledge and no decisions of its own. Alongside
251
+ the precedent store above, the other user-authored surface Keel loads on demand is domain guidance,
252
+ which lives in
183
253
  **lenses** you author under `keel/lenses/*.md` in your repo. Each lens is self-describing: it
184
254
  opens with an `Applies when:` line stating the signals that trigger it (file extensions, artifact
185
255
  shapes) and carries an `Execution and review checks` section. When a change's artifacts or Touch
@@ -212,6 +282,7 @@ keel guard status --json
212
282
  keel guard clear --json
213
283
 
214
284
  # Domain lenses — user-authored guidance in keel/lenses/
285
+ # (the other user-authored surface is the precedent store; see above)
215
286
  keel lenses list
216
287
  keel lenses add <name> [--force]
217
288
 
@@ -1,4 +1,4 @@
1
- <!-- keel:start version=5.4.0 -->
1
+ <!-- keel:start version=5.6.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.
@@ -126,8 +126,12 @@ artifacts:
126
126
  Keel agent. Keel-managed work executes in the current agent conversation;
127
127
  do not hand execution to another agent, subagent, or operator unless the
128
128
  selected task or user explicitly authorizes it.
129
- Autonomy boundary defaults to hard-stop. A pre-authorized fallback must
130
- state an exact reversible bound and the evidence required after use.
129
+ Autonomy boundary defaults to hard-stop for every action the repository
130
+ has not standing-authorized in `keel/config.yaml`; a declared action is
131
+ inherited only by a task that authored no boundary of its own, and the
132
+ capsule names the declaration as that entry's source. A pre-authorized
133
+ fallback must state an exact reversible bound and the evidence required
134
+ after use.
131
135
 
132
136
  Use `Coupling: none` by default. When `Coupling: required`, define one
133
137
  complete candidate, its allowed provisional failures, completion gate,
@@ -2,10 +2,14 @@
2
2
  Record only task-specific authority. Omitted fields inherit versioned
3
3
  defaults: Owner is the current Keel agent, Mode is implementation, Read
4
4
  is the change proposal/design/specs/tasks plus discovered repository
5
- context, Acceptance derives from Covers, autonomy defaults to hard-stop,
6
- Coupling defaults to none, helpers stay read-only/evidence-only, and
7
- commit, push, sync, archive, and cross-task continuation stay
8
- unauthorized. Declare a field only when it differs from these defaults. -->
5
+ context, Acceptance derives from Covers, Coupling defaults to none, and
6
+ helpers stay read-only/evidence-only. Autonomy defaults to hard-stop, and
7
+ commit, push, sync, archive, and cross-task continuation stay unauthorized,
8
+ EXCEPT where `keel/config.yaml` standing-authorizes an action: a task that
9
+ authors no `Autonomy boundary:` inherits that declaration, and the capsule
10
+ names the declaration as the entry's source. A standing authorization
11
+ removes the confirmation, never the gate, evidence, or Review.
12
+ Declare a field only when it differs from these defaults. -->
9
13
 
10
14
  ## 1. <!-- Task Group Name -->
11
15
 
package/bin/keel.js CHANGED
@@ -44,6 +44,11 @@ const {
44
44
  renderGuard,
45
45
  startGuard,
46
46
  } = require("../src/core/guard");
47
+ const {
48
+ STANDING_AUTHORIZATION_ACTIONS,
49
+ readPrecedentStore,
50
+ readStandingAuthorization,
51
+ } = require("../src/core/config");
47
52
 
48
53
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
49
54
  const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
@@ -1033,6 +1038,8 @@ function keelOpenSpecOverlay(action) {
1033
1038
  "- When implementation exposes a material expectation, acceptance boundary, or user-owned decision absent from durable authority, stop before implementing that choice, rerun `keel-align-expectations`, and reauthor the affected proposal/design/spec/task authority first.",
1034
1039
  "- A discovered repository fact that does not change accepted behavior or scope may be recorded and execution continues inside the existing task boundary without a product interview.",
1035
1040
  "- Invoke OpenSpec through `keel openspec` (for example `keel openspec validate`); a bare `openspec` command may not be on PATH.",
1041
+ "- Consult the repository's standing authorization in `keel/config.yaml` before asking the user to confirm a repository action: a standing-authorized action proceeds without a per-occurrence confirmation, and an undeclared action still requires the confirmation it requires today.",
1042
+ "- A standing authorization covers the action and never substitutes for a gate, evidence, or Review; it removes the confirmation, not the record, and it is not a trigger to perform the action.",
1036
1043
  ]
1037
1044
  : [
1038
1045
  "- The current agent owns final sync/archive decisions and must verify task evidence, follow-up ownership, and completion gates before proceeding.",
@@ -1043,6 +1050,8 @@ function keelOpenSpecOverlay(action) {
1043
1050
  "- Invoke OpenSpec through `keel openspec` (for example `keel openspec validate`); a bare `openspec` command may not be on PATH.",
1044
1051
  "- When `/opsx:sync` has already promoted the change's spec delta, run the archive with `--skip-specs` so the promoted delta is not re-applied; archive is not idempotent over an already-synced delta.",
1045
1052
  "- After archiving, run `keel guard clear` to drop the change's guard manifest; the read-only gate never clears it for you.",
1053
+ "- A repository that standing-authorizes `archive` in `keel/config.yaml` does not need the per-occurrence archive confirmation; a repository that declares nothing still needs it.",
1054
+ "- The completion gate and follow-up ownership checks still run unchanged under a standing authorization; it removes the confirmation, not the proof.",
1046
1055
  ];
1047
1056
 
1048
1057
  const lines = [
@@ -1344,10 +1353,12 @@ function runDoctor(options) {
1344
1353
 
1345
1354
  printTargetSurface(repo, options.target);
1346
1355
  printLensSurface(repo, options.target);
1356
+ const authorizationOk = printStandingAuthorizationSurface(repo);
1357
+ printPrecedentSurface(repo);
1347
1358
  printFastPrePushSurface(repo);
1348
1359
  printSourceRepoCliResolution(repo);
1349
1360
 
1350
- return checkStatus;
1361
+ return authorizationOk ? checkStatus : 1;
1351
1362
  }
1352
1363
 
1353
1364
  // Only meaningful in Keel's own repository: a bare `keel` resolves to the
@@ -1390,6 +1401,73 @@ function gitConfigHooksPath(repo) {
1390
1401
  return value || null;
1391
1402
  }
1392
1403
 
1404
+ function printStandingAuthorizationSurface(repo) {
1405
+ process.stdout.write("\nStanding authorization:\n");
1406
+ const { declared, unknown } = readStandingAuthorization(repo);
1407
+ if (unknown.length > 0) {
1408
+ printDoctorLine(
1409
+ "authorize",
1410
+ "failed",
1411
+ `keel/config.yaml declares unrecognized ${
1412
+ unknown.length === 1 ? "action" : "actions"
1413
+ }: ${unknown.join(", ")}; accepted names are `
1414
+ + `${STANDING_AUTHORIZATION_ACTIONS.join(", ")}. The whole declaration `
1415
+ + "authorizes nothing until it is corrected"
1416
+ );
1417
+ return false;
1418
+ }
1419
+ printDoctorLine(
1420
+ "authorize",
1421
+ declared.length > 0 ? "ok" : "none",
1422
+ declared.length > 0
1423
+ ? `declared in keel/config.yaml: ${declared.join(", ")}`
1424
+ : "undeclared; every action stays hard-stop"
1425
+ );
1426
+ for (const action of STANDING_AUTHORIZATION_ACTIONS) {
1427
+ printDoctorLine(
1428
+ action,
1429
+ declared.includes(action) ? "authorized" : "not authorized"
1430
+ );
1431
+ }
1432
+ return true;
1433
+ }
1434
+
1435
+ function printPrecedentSurface(repo) {
1436
+ process.stdout.write("\nPrecedent store:\n");
1437
+ const store = readPrecedentStore(repo);
1438
+ if (store.precedents.length === 0) {
1439
+ printDoctorLine(
1440
+ "precedents",
1441
+ "none",
1442
+ store.declared
1443
+ ? `declared at ${store.declared}, which holds no precedents here; `
1444
+ + "an absent store behaves exactly as an undeclared one"
1445
+ : "undeclared; no precedent informs any decision"
1446
+ );
1447
+ return;
1448
+ }
1449
+ const authorized = store.precedents.filter(
1450
+ (item) => item.status === "authorized"
1451
+ ).length;
1452
+ printDoctorLine("precedents", String(store.precedents.length), store.declared);
1453
+ printDoctorLine(
1454
+ "authorized",
1455
+ String(authorized),
1456
+ `${store.precedents.length - authorized} recorded, offered as a `
1457
+ + "recommendation rather than applied"
1458
+ );
1459
+ const incomplete = store.precedents.filter((item) => !item.complete);
1460
+ printDoctorLine(
1461
+ "incomplete",
1462
+ String(incomplete.length),
1463
+ incomplete.length > 0
1464
+ ? `missing a Rationale, so not applicable to any decision: ${incomplete
1465
+ .map((item) => item.name)
1466
+ .join(", ")}`
1467
+ : "every precedent states why, which is the part that transfers"
1468
+ );
1469
+ }
1470
+
1393
1471
  function printFastPrePushSurface(repo) {
1394
1472
  process.stdout.write("\nFast pre-push surface:\n");
1395
1473
  const fastCheck = readFastCheck(repo);
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.4.0",
5
+ "version": "5.6.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.4.0",
3
+ "version": "5.6.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.4.0",
3
+ "version": "5.6.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",
@@ -53,6 +53,61 @@ function emit(context, humanMessage) {
53
53
  // at session start is the one who must not mistake a projection for authority.
54
54
  const DISPOSABLE = "Disposable projection; OpenSpec and Git are the authority.";
55
55
 
56
+ // A pointer, never a body. The store grows without bound while the precedents
57
+ // relevant to any one session are a small subset, and this hook pays its cost
58
+ // on every session including post-compaction reinjection. Counts and freshness
59
+ // tell the agent the store exists and how stale it is; the precedents
60
+ // themselves load when a decision is actually being made.
61
+ //
62
+ // The reader is inlined rather than required from src/core because this script
63
+ // ships inside the plugin and must run without the CLI package resolvable.
64
+ function precedentPointer(cwd) {
65
+ let declared = null;
66
+ try {
67
+ const configPath = path.join(cwd, "keel", "config.yaml");
68
+ if (!fs.existsSync(configPath)) return null;
69
+ for (const line of fs.readFileSync(configPath, "utf8").split(/\r?\n/)) {
70
+ const stripped = line.trim();
71
+ if (stripped.startsWith("#")) continue;
72
+ const match = stripped.match(/^precedents\s*:\s*(.+?)\s*$/);
73
+ if (match) {
74
+ declared = match[1];
75
+ break;
76
+ }
77
+ }
78
+ if (!declared) return null;
79
+ const resolved = path.isAbsolute(declared)
80
+ ? declared
81
+ : path.resolve(cwd, declared);
82
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
83
+ return `precedent store declared at ${declared} but absent here; `
84
+ + "no precedent informs any decision.";
85
+ }
86
+ const files = fs
87
+ .readdirSync(resolved)
88
+ .filter((name) => name.endsWith(".md") && name !== "README.md");
89
+ let authorized = 0;
90
+ let newest = 0;
91
+ for (const name of files) {
92
+ const full = path.join(resolved, name);
93
+ if (/^-\s*Status:\s*authorized/mi.test(fs.readFileSync(full, "utf8"))) {
94
+ authorized += 1;
95
+ }
96
+ const mtime = fs.statSync(full).mtimeMs;
97
+ if (mtime > newest) newest = mtime;
98
+ }
99
+ const synced = newest
100
+ ? new Date(newest).toISOString().slice(0, 10)
101
+ : "never";
102
+ return `precedents: ${files.length} (${authorized} authorized, `
103
+ + `last synced ${synced}); bodies load at the decision, not here.`;
104
+ } catch {
105
+ // The projection never blocks a session. An unreadable store is the same
106
+ // as no store for this hook's purposes.
107
+ return null;
108
+ }
109
+ }
110
+
56
111
  // The Keel mark. A keel is the carina, the ridge on a bird's sternum, so the
57
112
  // animal that literally has one is a bird. Every cell is drawn from
58
113
  // U+2580–U+259F — the same block-element family as the host's own startup
@@ -238,6 +293,8 @@ function main() {
238
293
  + "does not guess among candidates."
239
294
  );
240
295
  }
296
+ const pointer = precedentPointer(cwd);
297
+ if (pointer) lines.push(`- ${pointer}`);
241
298
  lines.push(`- report this state ${DISCLOSURE}; it authorizes nothing.`);
242
299
  emit(lines.join("\n"), panel(human));
243
300
  return 0;
@@ -40,6 +40,37 @@ Accepted alignment routes to existing OpenSpec owners; create no separate alignm
40
40
  - specs own observable requirements and positive/negative/edge/failure scenarios.
41
41
  - tasks.md owns Covers, verification strategy and checks, scope, and stop boundaries that reference the accepted authority instead of duplicating chat prose.
42
42
 
43
+ ## Decision precedents
44
+
45
+ When the repository declares a precedent store, consult the precedent matching a decision before
46
+ escalating it, and record a new precedent when the user decides something the store does not cover.
47
+ A precedent store is user-authored and never bundled; a repository that declares none behaves
48
+ exactly as one without this section.
49
+
50
+ Record the reasoning, not only the conclusion. "Chose A" applies only to the situation literally
51
+ recorded; "chose A because B fails offline" can be applied to a case nobody has seen yet, and — just
52
+ as important — recognised as *not* applying when the new case is online. Only the reasoning transfers.
53
+ A precedent with no rationale is incomplete and is not applied.
54
+
55
+ Three rules govern use:
56
+
57
+ - **Cite only where you replaced a question.** Name the precedent you applied exactly when, without
58
+ it, you would otherwise have interrupted the user. Decisions that would not have interrupted them
59
+ are not cited, so that a citation always marks a decision made in the user's place rather than
60
+ running commentary.
61
+ - **Promotion is the user's act.** A precedent enters as `recorded` and is offered as a
62
+ recommendation while the question is still asked. To make one applicable without asking, propose
63
+ the promotion and name the precedent; it changes only when the user accepts. There is no usage
64
+ count, age, or other threshold that promotes anything, because a threshold crosses with nobody
65
+ watching.
66
+ - **A precedent answers a recurrence; it never reclassifies.** It may shorten a decision inside its
67
+ materiality category by supplying the recorded answer and its reasoning. It never moves a decision
68
+ out of the categories that require asking, and no accumulation of precedents makes a category
69
+ immaterial. A decision that resembles a precedent but sits in a different category is not a match.
70
+
71
+ A precedent informs a decision and never substitutes for a proof: gates, evidence, Review, and the
72
+ write guard are untouched by anything in the store.
73
+
43
74
  ## Domain lenses
44
75
 
45
76
  When the change signals a specific domain, look in `keel/lenses/` for a lens whose `Applies when:` header matches, and read only that lens before asking domain questions; do not load unrelated lenses. When no lens matches, or the repo defines none, proceed on the domain-agnostic path. Lenses are user-authored; scaffold the bundled starting points with `keel lenses add` (web, hardware, hardware-dsl).