@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 +73 -2
- package/assets/bootstrap/AGENTS.md +1 -1
- package/assets/openspec/schemas/keel-spec-driven/schema.yaml +6 -2
- package/assets/openspec/schemas/keel-spec-driven/templates/tasks.md +8 -4
- package/bin/keel.js +79 -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/plugins/keel/scripts/session-start.js +57 -0
- package/plugins/keel/skills/keel-align-expectations/SKILL.md +31 -0
- package/scripts/validate_plugin.py +855 -2
- package/src/core/config.js +105 -0
- package/src/core/task-contract.js +19 -0
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
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
130
|
-
|
|
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,
|
|
6
|
-
|
|
7
|
-
commit, push, sync, archive, and cross-task continuation stay
|
|
8
|
-
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keel",
|
|
3
|
-
"version": "5.
|
|
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.
|
|
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).
|