@ngockhoale/ukit 2.7.13 → 2.8.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/CHANGELOG.md +9 -0
- package/manifests/documentation.yaml +11 -0
- package/manifests/platform.full.yaml +182 -0
- package/manifests/platform.user.yaml +53 -0
- package/package.json +3 -1
- package/src/cli/commands/diff.js +4 -2
- package/src/cli/commands/doctor.js +22 -1
- package/src/cli/commands/install.js +10 -0
- package/src/cli/commands/memory.js +142 -3
- package/src/cli/commands/playbook.js +53 -0
- package/src/cli/index.js +7 -0
- package/src/core/memory/recordStore.js +81 -0
- package/src/core/memory/storeV2.js +16 -52
- package/src/core/memory/userMemory.js +111 -0
- package/src/core/paths.js +1 -0
- package/src/core/runInstallPipeline.js +96 -3
- package/src/core/runtimeConfig.js +170 -5
- package/src/core/userPaths.js +21 -0
- package/src/core/userPlaybooks.js +185 -0
- package/src/index/taskRouting.js +422 -21
- package/src/index/verificationPlan.js +17 -0
- package/src/manifest/validateManifest.js +19 -0
- package/templates/.claude/config/providers.md +1 -3
- package/templates/.claude/skills/principle-attack-the-premise/SKILL.md +16 -0
- package/templates/.claude/skills/principle-boundary-discipline/SKILL.md +16 -0
- package/templates/.claude/skills/principle-encode-lessons-in-structure/SKILL.md +16 -0
- package/templates/.claude/skills/principle-fix-root-causes/SKILL.md +18 -0
- package/templates/.claude/skills/principle-foundational-thinking/SKILL.md +17 -0
- package/templates/.claude/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/templates/.claude/skills/principle-laziness-protocol/SKILL.md +17 -0
- package/templates/.claude/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +16 -0
- package/templates/.claude/skills/principle-minimize-reader-load/SKILL.md +17 -0
- package/templates/.claude/skills/principle-model-the-domain/SKILL.md +16 -0
- package/templates/.claude/skills/principle-never-block-on-the-human/SKILL.md +16 -0
- package/templates/.claude/skills/principle-prove-it-works/SKILL.md +18 -0
- package/templates/.claude/skills/principle-sequence-verifiable-units/SKILL.md +16 -0
- package/templates/.claude/skills/principle-subtract-before-you-add/SKILL.md +16 -0
- package/templates/.claude/skills/principle-test-behavior-not-implementation/SKILL.md +18 -0
- package/templates/.claude/ukit/index/route-task.mjs +652 -28
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +238 -9
- package/templates/ukit/README.md +31 -0
- package/templates/ukit/storage/config.json +10 -0
- package/templates/user/README.md +21 -0
- package/templates/user/playbooks/bug-fix.md +18 -0
- package/templates/user/playbooks/issue-implementation.md +14 -0
- package/templates/user/storage/config.json +16 -0
package/src/index/taskRouting.js
CHANGED
|
@@ -10,7 +10,109 @@ import {
|
|
|
10
10
|
CONTRACT_RISK_PROFILES,
|
|
11
11
|
EXECUTION_CONTRACTS,
|
|
12
12
|
EXECUTION_MODE_ORDER,
|
|
13
|
+
MODEL_TIER_BY_CONTRACT,
|
|
13
14
|
} from '../core/executionContracts.js';
|
|
15
|
+
import { resolveModelRoles, readMergedRuntimeConfig } from '../core/runtimeConfig.js';
|
|
16
|
+
import { resolvePlaybook } from '../core/userPlaybooks.js';
|
|
17
|
+
|
|
18
|
+
// --- v3 route-side advisory blocks (docs/pstack/SPEC-playbook-todo.md, ------------------
|
|
19
|
+
// SPEC-principle-index.md, SPEC-model-roles.md). All three are additive stdout text —
|
|
20
|
+
// they never change mode selection. Static text is byte-stable for prompt-cache reuse.
|
|
21
|
+
// Mirrored literally in templates/.claude/ukit/index/route-task.mjs (parity-locked by
|
|
22
|
+
// tests/consistency/executionContractSync.test.js).
|
|
23
|
+
|
|
24
|
+
// SPEC-playbook-todo §2.1: numbered step lists the model copies verbatim into its todo
|
|
25
|
+
// list. Header carries the "new task" re-match rule (§2.5); tail carries the narrowed
|
|
26
|
+
// ask-surface (§2.4). Emitted on matching lanes only (§2.2).
|
|
27
|
+
export const WORKFLOW_POLICIES = Object.freeze({
|
|
28
|
+
'bug-fix': `You own this bug. Reproduce, root-cause, fix, verify on the same surface.
|
|
29
|
+
If the user says "new task", re-route — do not treat the message as the next step.
|
|
30
|
+
1. Reproduce it yourself on the matching surface — ask the user only with a stated
|
|
31
|
+
reason the surface cannot reach the target.
|
|
32
|
+
2. Binary-search the cause: form candidate hypotheses, rule them out until one
|
|
33
|
+
survives; confirm the surviving mechanism with runtime evidence.
|
|
34
|
+
3. Make the smallest fix that kills the mechanism — belt-and-suspenders that
|
|
35
|
+
"might help" is a hypothesis, not a fix; it does not ship.
|
|
36
|
+
4. Verify on the same surface: the original repro now passes. "Inconclusive" or
|
|
37
|
+
wrong-surface is not a pass. A unit test shows branch behavior, not bug absence.
|
|
38
|
+
5. Keep the rejected hypotheses — one line each, why ruled out.
|
|
39
|
+
Reply: what was broken, root cause, fix, how verified — paste failing-then-passing
|
|
40
|
+
repro output verbatim.
|
|
41
|
+
Ask the human only for: irreversible writes, a genuine preference call no experiment settles, or a real dead end. Everything else: do it, report it.`,
|
|
42
|
+
'issue-implementation': `You own this task. Normalize the goal, build, verify.
|
|
43
|
+
If the user says "new task", re-route — do not treat the message as the next step.
|
|
44
|
+
1. State the done condition as a checkable predicate before writing code.
|
|
45
|
+
2. Find the established analog — follow it unless you name why it does not fit.
|
|
46
|
+
3. Name the data shape and its organizing structure before writing logic.
|
|
47
|
+
4. Implement the smallest change satisfying the predicate.
|
|
48
|
+
5. Verify against the predicate on the real artifact — not "it compiles".
|
|
49
|
+
6. Widen once: check the impact surface the route named, no broader.
|
|
50
|
+
Reply: what changed, the predicate, the evidence it now holds.
|
|
51
|
+
Ask the human only for: irreversible writes, a genuine preference call no experiment settles, or a real dead end. Everything else: do it, report it.`,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Lane → policy (§2.2). tiny-fix/local-fix stay lean (Fast Path); review-release and
|
|
55
|
+
// informational carry no policy.
|
|
56
|
+
export const WORKFLOW_POLICY_BY_MODE = Object.freeze({
|
|
57
|
+
'find-cause': 'bug-fix',
|
|
58
|
+
'local-build': 'issue-implementation',
|
|
59
|
+
'shared-edit': 'issue-implementation',
|
|
60
|
+
'map-impact': 'issue-implementation',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// SPEC-principle-index §2.1: ~16-line index, one line per principle; group headers are
|
|
64
|
+
// the index's own compression. §2.3 citation contract is the trailing line.
|
|
65
|
+
export const PRINCIPLE_INDEX = `Principles (cite only leaves read this session; name the decision it changed):
|
|
66
|
+
Core — laziness-protocol: smallest change that solves it; bias to deletion.
|
|
67
|
+
foundational-thinking: pick core types/data structures before logic.
|
|
68
|
+
subtract-before-you-add: remove dead weight before building.
|
|
69
|
+
minimize-reader-load: collapse one-caller wrappers; shrink mutable scope.
|
|
70
|
+
attack-the-premise: 2+ fixes sharing one premise failed → question the premise.
|
|
71
|
+
Arch — model-the-domain: encode domain in structure, not scattered conditionals.
|
|
72
|
+
boundary-discipline: guards at boundaries; pure functions inside.
|
|
73
|
+
migrate-callers-then-delete: migrate every caller, delete old API same wave.
|
|
74
|
+
Verify — prove-it-works: verify the real artifact, not proxy/self-report/compiles.
|
|
75
|
+
fix-root-causes: reproduce first; no nil-guards that silence crashes.
|
|
76
|
+
sequence-verifiable-units: small units each ending verifiable.
|
|
77
|
+
test-behavior-not-implementation: assert what consumers observe.
|
|
78
|
+
Exec — guard-the-context-window: bulk to subagents; summaries in main thread.
|
|
79
|
+
never-block-on-the-human: proceed, report, course-correct after.
|
|
80
|
+
encode-lessons-in-structure: rule → lint/flag/script, not more prose.
|
|
81
|
+
When a principle shapes a decision, cite it and the choice it changed — only if you read the leaf this session.`;
|
|
82
|
+
|
|
83
|
+
// SPEC-model-roles §2.2: lanes that may delegate get the role block; Fast Path lanes
|
|
84
|
+
// (tiny-fix/local-fix) never delegate so the block would be dead context there.
|
|
85
|
+
export const MODEL_ROLES_DELEGATING_MODES = new Set([
|
|
86
|
+
'local-build',
|
|
87
|
+
'find-cause',
|
|
88
|
+
'shared-edit',
|
|
89
|
+
'map-impact',
|
|
90
|
+
'review-release',
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
// Principle index emission (§2.1): every non-trivial route — taskType non-trivial/
|
|
94
|
+
// shared-simple, or any mutating lane — except the Fast Path lanes, where a one-line
|
|
95
|
+
// edit does not need a principle index.
|
|
96
|
+
export function shouldEmitPrincipleIndex({ taskType = null, executionMode = null } = {}) {
|
|
97
|
+
if (executionMode === 'tiny-fix' || executionMode === 'local-fix') {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (taskType === 'non-trivial' || taskType === 'shared-simple') {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return Boolean(executionMode) && executionMode !== 'informational';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Formats the model-roles injection block from the resolved role map. Byte-stable for
|
|
107
|
+
// unchanged config (fixed role order, no timestamps).
|
|
108
|
+
export function formatModelRolesBlock(roles = null) {
|
|
109
|
+
const resolved = roles ?? resolveModelRoles(null).roles;
|
|
110
|
+
const panel = Array.isArray(resolved['review-panel'])
|
|
111
|
+
? resolved['review-panel'].join('+')
|
|
112
|
+
: String(resolved['review-panel']);
|
|
113
|
+
return `Model roles: code→${resolved.code}, judgment→${resolved.judgment}, review-panel→${panel},
|
|
114
|
+
fast-worker→${resolved['fast-worker']}, vision→${resolved.vision}. Delegate via role name; inherit-parent = same model as parent.`;
|
|
115
|
+
}
|
|
14
116
|
|
|
15
117
|
const MAX_ACTIVE_ROUTE_SKILLS = 2;
|
|
16
118
|
|
|
@@ -42,6 +144,272 @@ function deriveContextDocs({ taskType = null, intentMode = null } = {}) {
|
|
|
42
144
|
return unique(docs).slice(0, CONTEXT_DOCS_MAX);
|
|
43
145
|
}
|
|
44
146
|
|
|
147
|
+
// --- M01.1: additive ResolvedTaskRoute v1 (docs/pstack/CONTRACTS.md C01) -------------------
|
|
148
|
+
// Emitted only when `routing.routeSchema.stage` (runtime config, default "off") is not
|
|
149
|
+
// "off". All fields are additive on top of the existing routeSummary shape — legacy
|
|
150
|
+
// top-level fields are never removed or renamed, so older consumers keep working.
|
|
151
|
+
export const ROUTE_VERSION = 1;
|
|
152
|
+
export const ROUTE_CONTRACT_VERSION = 1;
|
|
153
|
+
export const ROUTE_SCHEMA_STAGES = new Set(['off', 'shadow', 'canary', 'default']);
|
|
154
|
+
export const ROUTE_INTENT_KINDS = new Set(['informational', 'delivery', 'mutation', 'investigation', 'review']);
|
|
155
|
+
export const ROUTE_MUTABILITIES = new Set(['read-only', 'mutating', 'mixed']);
|
|
156
|
+
export const ROUTE_RIGOR_LEVELS = new Set(['R0', 'R1', 'R2', 'R3', 'R4']);
|
|
157
|
+
export const ROUTE_MODEL_TIERS = new Set(['lite', 'code', 'smart']);
|
|
158
|
+
// 'informational' is a real router emission (no completion contract) even though it is
|
|
159
|
+
// not part of the seven-lane EXECUTION_MODE_ORDER ladder.
|
|
160
|
+
export const ROUTE_EXECUTION_MODES = [...EXECUTION_MODE_ORDER, 'informational'];
|
|
161
|
+
const ROUTE_ESCALATION_CEILING = 'review-release';
|
|
162
|
+
const ROUTE_GOAL_MAX_LENGTH = 240;
|
|
163
|
+
|
|
164
|
+
// Stage keys treat absence as "off" (MIGRATION_ROLLBACK named-keys table). Unknown or
|
|
165
|
+
// malformed values degrade to "off" — the conservative reading that keeps the route
|
|
166
|
+
// byte-identical to the pre-M01.1 shape.
|
|
167
|
+
export function resolveRouteSchemaStage(config = null) {
|
|
168
|
+
const stage = config?.routing?.routeSchema?.stage;
|
|
169
|
+
return ROUTE_SCHEMA_STAGES.has(stage) ? stage : 'off';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
// A bare delivery command ("push this to git", "đẩy bộ này lên git") performs no
|
|
174
|
+
// repository mutation the ledger could ever receipt. With no edit/review/debug/build
|
|
175
|
+
// signal present, routing it to an investigation lane fabricates write debt and the
|
|
176
|
+
// completion gate then demands an edit that cannot exist. Extracted from
|
|
177
|
+
// deriveExecutionMode so the C01 intent.kind mapping can reuse the identical predicate
|
|
178
|
+
// (delivery-only → 'delivery') instead of duplicating the regexes.
|
|
179
|
+
function isDeliveryOnlyRequest({ signalText = '', scores = {}, targetFile = null } = {}) {
|
|
180
|
+
const signalRaw = String(signalText || '').toLowerCase();
|
|
181
|
+
const deliveryWordSignal = /\bgit\s+push\b/.test(signalRaw)
|
|
182
|
+
|| /\bpush\b[^\n]{0,60}\b(?:git|github|gitlab|remote|origin|repo)\b/.test(signalRaw)
|
|
183
|
+
|| /\b(?:git|github|gitlab|remote|origin|repo)\b[^\n]{0,60}\bpush\b/.test(signalRaw)
|
|
184
|
+
|| /\bday\b(?:\s+\S+){0,3}?\s+len\b/.test(signalRaw);
|
|
185
|
+
return deliveryWordSignal
|
|
186
|
+
&& scores.editCertainty === 0
|
|
187
|
+
&& !scores.implementSignal
|
|
188
|
+
&& !scores.reviewSignal
|
|
189
|
+
&& !scores.debugSignal
|
|
190
|
+
&& !scores.failureSignal
|
|
191
|
+
&& !scores.impactSignal
|
|
192
|
+
&& !scores.buildSignal
|
|
193
|
+
&& !scores.directTransformSignal
|
|
194
|
+
&& !scores.smallFixSignal
|
|
195
|
+
&& !scores.sharedRisk
|
|
196
|
+
&& !targetFile;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// CONTRACTS.md "Intent vocabulary mapping": taskType informs mode priors, not
|
|
200
|
+
// intent.kind; intentMode maps to kind as question/explanation → informational,
|
|
201
|
+
// ship/deliver → delivery, code change → mutation, root-cause/diagnosis →
|
|
202
|
+
// investigation, review/audit → review.
|
|
203
|
+
function deriveRouteIntentKind({ executionMode = null, intentMode = null, deliveryOnly = false } = {}) {
|
|
204
|
+
if (executionMode === 'find-cause') return 'investigation';
|
|
205
|
+
if (executionMode === 'review-release') return 'review';
|
|
206
|
+
if (executionMode === 'informational') return deliveryOnly ? 'delivery' : 'informational';
|
|
207
|
+
if (executionMode) return 'mutation';
|
|
208
|
+
if (intentMode === 'review-specific') return 'review';
|
|
209
|
+
if (intentMode === 'debug-specific') return 'investigation';
|
|
210
|
+
if (intentMode === 'implement-specific' || intentMode === 'docs-specific') return 'mutation';
|
|
211
|
+
return 'informational';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Existing read-only vs mutating classification: only the informational lane carries
|
|
215
|
+
// no write debt; every contract lane is mutating.
|
|
216
|
+
function deriveRouteMutability(executionMode = null) {
|
|
217
|
+
return executionMode && executionMode !== 'informational' ? 'mutating' : 'read-only';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function compactRouteGoal(text = '') {
|
|
221
|
+
const goal = String(text || '').trim();
|
|
222
|
+
if (!goal) return null;
|
|
223
|
+
return goal.length > ROUTE_GOAL_MAX_LENGTH ? `${goal.slice(0, ROUTE_GOAL_MAX_LENGTH)}…` : goal;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Builds the additive C01 groups for one resolved route. rigor stays null until
|
|
227
|
+
// M01.2 derives it; ceremonyBudget/capabilityPolicy are empty shaped objects M02/M01.2
|
|
228
|
+
// populate; escalation.current mirrors the selected mode.
|
|
229
|
+
function buildResolvedRouteFields({
|
|
230
|
+
routingContext = {},
|
|
231
|
+
activeSkillIds = [],
|
|
232
|
+
executionMode = null,
|
|
233
|
+
executionContract = null,
|
|
234
|
+
completionState = null,
|
|
235
|
+
} = {}) {
|
|
236
|
+
const signalText = buildRouteSignalText(routingContext.promptText, routingContext.commandText);
|
|
237
|
+
const deliveryOnly = isDeliveryOnlyRequest({
|
|
238
|
+
signalText,
|
|
239
|
+
scores: routingContext.executionScores ?? {},
|
|
240
|
+
targetFile: routingContext.targetFile ?? null,
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
routeVersion: ROUTE_VERSION,
|
|
244
|
+
intent: {
|
|
245
|
+
kind: deriveRouteIntentKind({
|
|
246
|
+
executionMode,
|
|
247
|
+
intentMode: routingContext.intentMode ?? null,
|
|
248
|
+
deliveryOnly,
|
|
249
|
+
}),
|
|
250
|
+
mutability: deriveRouteMutability(executionMode),
|
|
251
|
+
goal: compactRouteGoal(routingContext.lastExplicitUserPromptText ?? routingContext.promptText),
|
|
252
|
+
doneConditions: [],
|
|
253
|
+
},
|
|
254
|
+
execution: {
|
|
255
|
+
mode: executionMode,
|
|
256
|
+
rigor: null,
|
|
257
|
+
phase: null,
|
|
258
|
+
contractVersion: ROUTE_CONTRACT_VERSION,
|
|
259
|
+
modelTier: executionContract?.modelTier ?? MODEL_TIER_BY_CONTRACT[executionMode] ?? null,
|
|
260
|
+
},
|
|
261
|
+
evidence: {
|
|
262
|
+
observations: [],
|
|
263
|
+
riskSignals: [],
|
|
264
|
+
activationReasons: [],
|
|
265
|
+
suppressionReasons: [],
|
|
266
|
+
completionRequirements: unique(completionState?.missingEvidence ?? []),
|
|
267
|
+
},
|
|
268
|
+
ceremonyBudget: {
|
|
269
|
+
policyVersion: ROUTE_CONTRACT_VERSION,
|
|
270
|
+
rigor: null,
|
|
271
|
+
limits: {},
|
|
272
|
+
consumed: {},
|
|
273
|
+
exceptions: [],
|
|
274
|
+
},
|
|
275
|
+
capabilityPolicy: {
|
|
276
|
+
policyVersion: ROUTE_CONTRACT_VERSION,
|
|
277
|
+
required: [],
|
|
278
|
+
recommended: [],
|
|
279
|
+
suppressed: [],
|
|
280
|
+
activeSkillIds: unique(activeSkillIds),
|
|
281
|
+
},
|
|
282
|
+
escalation: {
|
|
283
|
+
current: { mode: executionMode, rigor: null },
|
|
284
|
+
ceiling: ROUTE_ESCALATION_CEILING,
|
|
285
|
+
triggers: [],
|
|
286
|
+
history: [],
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Plain-JS validator for the additive C01 groups. Returns { valid, errors }; it never
|
|
292
|
+
// throws and never inspects legacy fields — old consumers may carry anything else.
|
|
293
|
+
export function validateResolvedRoute(route = null) {
|
|
294
|
+
const errors = [];
|
|
295
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
296
|
+
if (!isObject(route)) {
|
|
297
|
+
return { valid: false, errors: ['route must be an object.'] };
|
|
298
|
+
}
|
|
299
|
+
if (route.routeVersion !== ROUTE_VERSION) {
|
|
300
|
+
errors.push(`routeVersion must be ${ROUTE_VERSION}.`);
|
|
301
|
+
}
|
|
302
|
+
if (!isObject(route.intent)) {
|
|
303
|
+
errors.push('intent must be an object.');
|
|
304
|
+
} else {
|
|
305
|
+
if (!ROUTE_INTENT_KINDS.has(route.intent.kind)) {
|
|
306
|
+
errors.push(`intent.kind must be one of: ${[...ROUTE_INTENT_KINDS].join(', ')}.`);
|
|
307
|
+
}
|
|
308
|
+
if (!ROUTE_MUTABILITIES.has(route.intent.mutability)) {
|
|
309
|
+
errors.push(`intent.mutability must be one of: ${[...ROUTE_MUTABILITIES].join(', ')}.`);
|
|
310
|
+
}
|
|
311
|
+
if (route.intent.goal !== null && typeof route.intent.goal !== 'string') {
|
|
312
|
+
errors.push('intent.goal must be a string or null.');
|
|
313
|
+
}
|
|
314
|
+
if (!Array.isArray(route.intent.doneConditions)) {
|
|
315
|
+
errors.push('intent.doneConditions must be an array.');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!isObject(route.execution)) {
|
|
319
|
+
errors.push('execution must be an object.');
|
|
320
|
+
} else {
|
|
321
|
+
if (route.execution.mode !== null && !ROUTE_EXECUTION_MODES.includes(route.execution.mode)) {
|
|
322
|
+
errors.push(`execution.mode must be null or one of: ${ROUTE_EXECUTION_MODES.join(', ')}.`);
|
|
323
|
+
}
|
|
324
|
+
if (route.execution.rigor !== null && !ROUTE_RIGOR_LEVELS.has(route.execution.rigor)) {
|
|
325
|
+
errors.push(`execution.rigor must be null or one of: ${[...ROUTE_RIGOR_LEVELS].join(', ')}.`);
|
|
326
|
+
}
|
|
327
|
+
if (route.execution.contractVersion !== ROUTE_CONTRACT_VERSION) {
|
|
328
|
+
errors.push(`execution.contractVersion must be ${ROUTE_CONTRACT_VERSION}.`);
|
|
329
|
+
}
|
|
330
|
+
if (route.execution.modelTier !== null && !ROUTE_MODEL_TIERS.has(route.execution.modelTier)) {
|
|
331
|
+
errors.push(`execution.modelTier must be null or one of: ${[...ROUTE_MODEL_TIERS].join(', ')}.`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!isObject(route.evidence)) {
|
|
335
|
+
errors.push('evidence must be an object.');
|
|
336
|
+
} else {
|
|
337
|
+
for (const key of ['observations', 'riskSignals', 'activationReasons', 'suppressionReasons', 'completionRequirements']) {
|
|
338
|
+
if (!Array.isArray(route.evidence[key])) {
|
|
339
|
+
errors.push(`evidence.${key} must be an array.`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!isObject(route.ceremonyBudget)) {
|
|
344
|
+
errors.push('ceremonyBudget must be an object.');
|
|
345
|
+
} else {
|
|
346
|
+
if (route.ceremonyBudget.rigor !== null && !ROUTE_RIGOR_LEVELS.has(route.ceremonyBudget.rigor)) {
|
|
347
|
+
errors.push(`ceremonyBudget.rigor must be null or one of: ${[...ROUTE_RIGOR_LEVELS].join(', ')}.`);
|
|
348
|
+
}
|
|
349
|
+
if (!isObject(route.ceremonyBudget.limits)) {
|
|
350
|
+
errors.push('ceremonyBudget.limits must be an object.');
|
|
351
|
+
}
|
|
352
|
+
if (!isObject(route.ceremonyBudget.consumed)) {
|
|
353
|
+
errors.push('ceremonyBudget.consumed must be an object.');
|
|
354
|
+
}
|
|
355
|
+
if (!Array.isArray(route.ceremonyBudget.exceptions)) {
|
|
356
|
+
errors.push('ceremonyBudget.exceptions must be an array.');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!isObject(route.capabilityPolicy)) {
|
|
360
|
+
errors.push('capabilityPolicy must be an object.');
|
|
361
|
+
} else {
|
|
362
|
+
for (const key of ['required', 'recommended', 'suppressed', 'activeSkillIds']) {
|
|
363
|
+
if (!Array.isArray(route.capabilityPolicy[key])) {
|
|
364
|
+
errors.push(`capabilityPolicy.${key} must be an array.`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (!isObject(route.escalation)) {
|
|
369
|
+
errors.push('escalation must be an object.');
|
|
370
|
+
} else {
|
|
371
|
+
if (!isObject(route.escalation.current)) {
|
|
372
|
+
errors.push('escalation.current must be an object.');
|
|
373
|
+
} else {
|
|
374
|
+
if (route.escalation.current.mode !== null && !ROUTE_EXECUTION_MODES.includes(route.escalation.current.mode)) {
|
|
375
|
+
errors.push(`escalation.current.mode must be null or one of: ${ROUTE_EXECUTION_MODES.join(', ')}.`);
|
|
376
|
+
}
|
|
377
|
+
if (route.escalation.current.rigor !== null && !ROUTE_RIGOR_LEVELS.has(route.escalation.current.rigor)) {
|
|
378
|
+
errors.push(`escalation.current.rigor must be null or one of: ${[...ROUTE_RIGOR_LEVELS].join(', ')}.`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (!Array.isArray(route.escalation.triggers)) {
|
|
382
|
+
errors.push('escalation.triggers must be an array.');
|
|
383
|
+
}
|
|
384
|
+
if (!Array.isArray(route.escalation.history)) {
|
|
385
|
+
errors.push('escalation.history must be an array.');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { valid: errors.length === 0, errors };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Compact serialization whitelist (C01 compatibility rule): adapter/runtime consumers
|
|
392
|
+
// get a bounded view that may omit verbose evidence but never mode, rigor, required
|
|
393
|
+
// completion evidence, or escalation state. Returns null for legacy (stage-off)
|
|
394
|
+
// summaries so compact output stays byte-identical when the schema stage is off.
|
|
395
|
+
export function compactResolvedRoute(routeSummary = null) {
|
|
396
|
+
if (!routeSummary || typeof routeSummary !== 'object' || routeSummary.routeVersion == null) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
routeVersion: routeSummary.routeVersion,
|
|
401
|
+
intent: routeSummary.intent ?? null,
|
|
402
|
+
execution: routeSummary.execution ?? null,
|
|
403
|
+
evidence: {
|
|
404
|
+
completionRequirements: unique(routeSummary.evidence?.completionRequirements ?? []),
|
|
405
|
+
},
|
|
406
|
+
ceremonyBudget: routeSummary.ceremonyBudget ?? null,
|
|
407
|
+
capabilityPolicy: routeSummary.capabilityPolicy ?? null,
|
|
408
|
+
escalation: routeSummary.escalation ?? null,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
45
413
|
export async function deriveTaskRoute({
|
|
46
414
|
rootDir = process.cwd(),
|
|
47
415
|
promptText = '',
|
|
@@ -51,8 +419,15 @@ export async function deriveTaskRoute({
|
|
|
51
419
|
lastExplicitUserPromptText = '',
|
|
52
420
|
commandNamespace = '.claude',
|
|
53
421
|
autonomyLevel = 'balanced',
|
|
422
|
+
runtimeConfig = null,
|
|
423
|
+
homeDir = undefined,
|
|
54
424
|
} = {}) {
|
|
55
425
|
const absoluteRoot = path.resolve(rootDir);
|
|
426
|
+
// M01.1: additive route-schema stage. Absent config / absent key / unknown value all
|
|
427
|
+
// resolve to "off" — the route then stays byte-identical to the pre-M01.1 shape.
|
|
428
|
+
const resolvedRuntimeConfig = runtimeConfig
|
|
429
|
+
?? await readMergedRuntimeConfig(absoluteRoot, { homeDir });
|
|
430
|
+
const routeSchemaStage = resolveRouteSchemaStage(resolvedRuntimeConfig);
|
|
56
431
|
const normalizedPrompt = String(promptText || '').trim();
|
|
57
432
|
const normalizedCommand = String(commandText || '').trim();
|
|
58
433
|
const normalizedTarget = normalizeRelativeFile(absoluteRoot, targetFile);
|
|
@@ -177,6 +552,13 @@ export async function deriveTaskRoute({
|
|
|
177
552
|
? await checkHandoffBudget(absoluteRoot)
|
|
178
553
|
: null;
|
|
179
554
|
const worklogBudget = await checkWorklogBudget(absoluteRoot);
|
|
555
|
+
// SPEC FR-009: the lane's workflow policy resolves through the playbook layer —
|
|
556
|
+
// project playbook > user playbook > builtin. resolvePlaybook already falls back to
|
|
557
|
+
// the builtin table, so a null result only means the lane has no policy at all.
|
|
558
|
+
const workflowPolicyName = WORKFLOW_POLICY_BY_MODE[executionMode] ?? null;
|
|
559
|
+
const resolvedWorkflowPolicy = workflowPolicyName
|
|
560
|
+
? await resolvePlaybook(workflowPolicyName, { projectRoot: absoluteRoot, homeDir })
|
|
561
|
+
: null;
|
|
180
562
|
const routeSummary = buildRouteSummary({
|
|
181
563
|
activeSkills,
|
|
182
564
|
routingContext: {
|
|
@@ -197,6 +579,9 @@ export async function deriveTaskRoute({
|
|
|
197
579
|
nextAction,
|
|
198
580
|
handoffBudget,
|
|
199
581
|
worklogBudget,
|
|
582
|
+
routeSchemaStage,
|
|
583
|
+
runtimeConfig: resolvedRuntimeConfig,
|
|
584
|
+
resolvedWorkflowPolicy,
|
|
200
585
|
});
|
|
201
586
|
const approachSelector = routeSummary?.approachSelector ?? null;
|
|
202
587
|
|
|
@@ -235,6 +620,9 @@ export function buildRouteSummary({
|
|
|
235
620
|
handoffBudget = null,
|
|
236
621
|
worklogBudget = null,
|
|
237
622
|
contextDocs = null,
|
|
623
|
+
routeSchemaStage = 'off',
|
|
624
|
+
runtimeConfig = null,
|
|
625
|
+
resolvedWorkflowPolicy = null,
|
|
238
626
|
} = {}) {
|
|
239
627
|
const autonomyLevel = routingContext.autonomyLevel ?? 'balanced';
|
|
240
628
|
const delegationRecommendation = deriveDelegationRecommendation({
|
|
@@ -279,6 +667,15 @@ export function buildRouteSummary({
|
|
|
279
667
|
nextActionType: nextAction?.type ?? null,
|
|
280
668
|
completionState,
|
|
281
669
|
});
|
|
670
|
+
const resolvedRouteFields = routeSchemaStage !== 'off'
|
|
671
|
+
? buildResolvedRouteFields({
|
|
672
|
+
routingContext,
|
|
673
|
+
activeSkillIds: activeSkills.map((entry) => entry.id),
|
|
674
|
+
executionMode,
|
|
675
|
+
executionContract,
|
|
676
|
+
completionState,
|
|
677
|
+
})
|
|
678
|
+
: null;
|
|
282
679
|
const helperHint = compactHelperHint(
|
|
283
680
|
compactHelperLane
|
|
284
681
|
? contextRecommendation?.command
|
|
@@ -298,6 +695,19 @@ export function buildRouteSummary({
|
|
|
298
695
|
const docsSegment = resolvedContextDocs.length > 0
|
|
299
696
|
? `docs=[${resolvedContextDocs.slice(0, CONTEXT_DOCS_MAX).map((p) => path.posix.basename(String(p).replaceAll('\\', '/'))).join(',')}]`
|
|
300
697
|
: null;
|
|
698
|
+
// v3 advisory blocks (SPEC-playbook-todo / SPEC-principle-index / SPEC-model-roles):
|
|
699
|
+
// additive stdout text only — never change mode selection.
|
|
700
|
+
const workflowPolicyName = WORKFLOW_POLICY_BY_MODE[executionMode] ?? null;
|
|
701
|
+
const workflowPolicy = workflowPolicyName
|
|
702
|
+
? (resolvedWorkflowPolicy?.body ?? WORKFLOW_POLICIES[workflowPolicyName])
|
|
703
|
+
: null;
|
|
704
|
+
const principleIndex = shouldEmitPrincipleIndex({ taskType, executionMode })
|
|
705
|
+
? PRINCIPLE_INDEX
|
|
706
|
+
: null;
|
|
707
|
+
const modelRolesResolution = resolveModelRoles(runtimeConfig);
|
|
708
|
+
const modelRolesBlock = MODEL_ROLES_DELEGATING_MODES.has(executionMode)
|
|
709
|
+
? formatModelRolesBlock(modelRolesResolution.roles)
|
|
710
|
+
: null;
|
|
301
711
|
const summaryLine = [
|
|
302
712
|
routingContext.taskType ? `task=${routingContext.taskType}` : null,
|
|
303
713
|
handoffFile ? `handoff=${handoffFile}` : null,
|
|
@@ -308,6 +718,7 @@ export function buildRouteSummary({
|
|
|
308
718
|
editGuardHint ? `editGuard=${editGuardHint}` : null,
|
|
309
719
|
delegationRecommendation?.hint ? `delegate=${delegationRecommendation.hint}` : null,
|
|
310
720
|
policyMode ? `policy=${policyMode}` : null,
|
|
721
|
+
workflowPolicyName ? `workflow=${workflowPolicyName}` : null,
|
|
311
722
|
handoffBudget?.warning ? `budget=${handoffBudget.warning}` : null,
|
|
312
723
|
worklogBudget?.warning ? `budget=${worklogBudget.warning}` : null,
|
|
313
724
|
].filter(Boolean).join(' | ');
|
|
@@ -324,6 +735,8 @@ export function buildRouteSummary({
|
|
|
324
735
|
approachSelector,
|
|
325
736
|
executionContract,
|
|
326
737
|
completionState,
|
|
738
|
+
// M01.1 additive C01 groups — present only when routing.routeSchema.stage != "off".
|
|
739
|
+
...(resolvedRouteFields ?? {}),
|
|
327
740
|
continuationState,
|
|
328
741
|
autonomyLevel,
|
|
329
742
|
continuousExecution,
|
|
@@ -337,6 +750,13 @@ export function buildRouteSummary({
|
|
|
337
750
|
helperHint,
|
|
338
751
|
contextMode,
|
|
339
752
|
contextDocs: resolvedContextDocs,
|
|
753
|
+
workflowPolicyName,
|
|
754
|
+
workflowPolicy,
|
|
755
|
+
// TASK-007: emitted only when a policy is emitted — the stage-off route shape
|
|
756
|
+
// stays unchanged on lanes that carry no workflow policy.
|
|
757
|
+
...(workflowPolicy ? { workflowPolicySource: resolvedWorkflowPolicy?.source ?? 'builtin' } : {}),
|
|
758
|
+
principleIndex,
|
|
759
|
+
modelRolesBlock,
|
|
340
760
|
line: summaryLine || 'task=unknown',
|
|
341
761
|
};
|
|
342
762
|
}
|
|
@@ -515,27 +935,8 @@ function deriveExecutionMode({
|
|
|
515
935
|
const strongImpactLead = scores.sharedRisk
|
|
516
936
|
&& (scores.impactSignal || /\b(check all affected|map all affected|across all affected)\b/.test(raw));
|
|
517
937
|
const boundedLocalBuildCandidate = scores.buildSignal && scores.boundedEditSignal && scores.explicitTarget && !scores.sharedRisk;
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
// build signal present, routing it to an investigation lane fabricates write debt
|
|
521
|
-
// and the completion gate then demands an edit that cannot exist. Delivery-only
|
|
522
|
-
// requests carry no completion contract instead.
|
|
523
|
-
const deliveryWordSignal = /\bgit\s+push\b/.test(signalRaw)
|
|
524
|
-
|| /\bpush\b[^\n]{0,60}\b(?:git|github|gitlab|remote|origin|repo)\b/.test(signalRaw)
|
|
525
|
-
|| /\b(?:git|github|gitlab|remote|origin|repo)\b[^\n]{0,60}\bpush\b/.test(signalRaw)
|
|
526
|
-
|| /\bday\b(?:\s+\S+){0,3}?\s+len\b/.test(signalRaw);
|
|
527
|
-
const deliveryOnlySignal = deliveryWordSignal
|
|
528
|
-
&& scores.editCertainty === 0
|
|
529
|
-
&& !scores.implementSignal
|
|
530
|
-
&& !scores.reviewSignal
|
|
531
|
-
&& !scores.debugSignal
|
|
532
|
-
&& !scores.failureSignal
|
|
533
|
-
&& !scores.impactSignal
|
|
534
|
-
&& !scores.buildSignal
|
|
535
|
-
&& !scores.directTransformSignal
|
|
536
|
-
&& !scores.smallFixSignal
|
|
537
|
-
&& !scores.sharedRisk
|
|
538
|
-
&& !targetFile;
|
|
938
|
+
// Delivery-only requests carry no completion contract (see isDeliveryOnlyRequest).
|
|
939
|
+
const deliveryOnlySignal = isDeliveryOnlyRequest({ signalText: signalRaw, scores, targetFile });
|
|
539
940
|
// A pure consultation question ("Rồi việc kế tiếp của tôi phải làm gì…?", "how do I
|
|
540
941
|
// measure this on another machine?") asks for an answer, not a repository change. The
|
|
541
942
|
// signal regexes are English-only, so a Vietnamese (or signal-free English) question
|
|
@@ -5,6 +5,9 @@ import { detectPackageManagerFromFingerprint, getDeclaredPackageManager } from '
|
|
|
5
5
|
import { resolveContext } from './resolveContext.js';
|
|
6
6
|
|
|
7
7
|
const RISKY_SKILL_IDS = new Set(['discover-security', 'repo-maintenance']);
|
|
8
|
+
// SPEC-typed-verdicts §2.5: commands matching a test runner can mint a
|
|
9
|
+
// `test-verified` verdict; everything else a plan routes is `check-only`.
|
|
10
|
+
const TEST_RUNNER_COMMAND = /(?:^|\s)(?:vitest|jest|mocha|ava|pytest|py\.test)(?:\s|$)|(?:npm|pnpm|yarn|bun)(?:\s+run)?\s+test(?:\s|$)/i;
|
|
8
11
|
const TRACKED_VERIFICATION_FILES = [
|
|
9
12
|
'package.json',
|
|
10
13
|
'package-lock.json',
|
|
@@ -144,6 +147,19 @@ export async function deriveVerificationPlan({
|
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
const primaryCommandList = unique(commands);
|
|
150
|
+
// SPEC-typed-verdicts §2.5: each routed command carries the verdict kind it can
|
|
151
|
+
// honestly produce — test runners prove `test-verified`; type-check/lint-only
|
|
152
|
+
// commands are `check-only` and cannot satisfy lanes requiring runtime proof.
|
|
153
|
+
const verdictKind = {};
|
|
154
|
+
for (const command of [...primaryCommandList, ...fallbackCommands]) {
|
|
155
|
+
verdictKind[command] = TEST_RUNNER_COMMAND.test(command) ? 'test-verified' : 'check-only';
|
|
156
|
+
}
|
|
157
|
+
const laneRequiresRuntimeProof = !docsOnly
|
|
158
|
+
&& (risky || primaryTargets.some((filePath) => !isTestLikeFile(filePath)));
|
|
159
|
+
if (laneRequiresRuntimeProof
|
|
160
|
+
&& primaryCommandList.every((command) => verdictKind[command] === 'check-only')) {
|
|
161
|
+
notes.push('verify: check-only insufficient for this lane — verify on the real surface (run the feature / inspect the artifact).');
|
|
162
|
+
}
|
|
147
163
|
const fallbackCommandList = unique(fallbackCommands.filter((command) => !primaryCommandList.includes(command)));
|
|
148
164
|
const reasonList = unique(reasons);
|
|
149
165
|
const noteList = unique(notes);
|
|
@@ -156,6 +172,7 @@ export async function deriveVerificationPlan({
|
|
|
156
172
|
fallbackCommands: fallbackCommandList,
|
|
157
173
|
reasons: reasonList,
|
|
158
174
|
notes: noteList,
|
|
175
|
+
verdictKind,
|
|
159
176
|
executionPolicy: deriveExecutionPolicy({
|
|
160
177
|
taskType: effectiveTaskType,
|
|
161
178
|
mode,
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
|
|
3
3
|
const VALID_ITEM_TYPES = new Set(['command', 'skill', 'agent', 'hook', 'config', 'link']);
|
|
4
4
|
const VALID_MERGE_STRATEGIES = new Set(['overwrite_with_backup', 'skip', 'append', 'prepend', 'merge_env_overwrite_with_backup']);
|
|
5
|
+
const VALID_ITEM_LEVELS = new Set(['project', 'user']);
|
|
5
6
|
|
|
6
7
|
function isPlainObject(value) {
|
|
7
8
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -33,6 +34,10 @@ export function validateManifest(manifest) {
|
|
|
33
34
|
errors.push('Manifest `name` must be a non-empty string.');
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
if (manifest.level !== undefined && manifest.level !== 'user') {
|
|
38
|
+
errors.push("Manifest `level` must be 'user' when provided.");
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
if (!Array.isArray(manifest.items) || manifest.items.length === 0) {
|
|
37
42
|
errors.push('Manifest `items` must be a non-empty array.');
|
|
38
43
|
return { valid: errors.length === 0, errors };
|
|
@@ -91,6 +96,20 @@ export function validateManifest(manifest) {
|
|
|
91
96
|
);
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
if (item.level !== undefined && !VALID_ITEM_LEVELS.has(item.level)) {
|
|
100
|
+
errors.push(`${prefix}.level must be one of: ${Array.from(VALID_ITEM_LEVELS).join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (manifest.level === 'user' || item.level === 'user') {
|
|
104
|
+
if (item.type === 'link') {
|
|
105
|
+
errors.push(`${prefix}.type must not be 'link' for user-level items.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (item.mergeStrategy !== 'skip') {
|
|
109
|
+
errors.push(`${prefix}.mergeStrategy must be 'skip' for user-level items.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
if (typeof item.enabledByDefault !== 'boolean') {
|
|
95
114
|
errors.push(`${prefix}.enabledByDefault must be a boolean.`);
|
|
96
115
|
}
|
|
@@ -11,9 +11,7 @@ Provider: **UNIC**
|
|
|
11
11
|
|
|
12
12
|
## API Key
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
anh-khoa-rat-dep-trai
|
|
16
|
-
```
|
|
14
|
+
Set the provider credential through the runtime's private environment or user-level secret configuration. Never commit a real key to the repository or generated project files.
|
|
17
15
|
|
|
18
16
|
## Available Models
|
|
19
17
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: principle-attack-the-premise
|
|
3
|
+
description: When two or more fixes sharing one premise have failed, question the premise instead of attempting a third fix. Apply during debugging and repeated-failure loops.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Attack the Premise
|
|
8
|
+
|
|
9
|
+
**Why:** Repeated failures on the same approach usually mean the shared assumption is wrong, not that the attempts were unlucky. A third fix on a false premise is a third failure.
|
|
10
|
+
|
|
11
|
+
- Track what every failed fix assumed; the common assumption is the suspect.
|
|
12
|
+
- After two failures sharing a premise, stop fixing and test the premise directly.
|
|
13
|
+
- Reproduce the premise's prediction in isolation before building on it again.
|
|
14
|
+
- Escalate with the premise named, not just "it still fails."
|
|
15
|
+
|
|
16
|
+
**Test:** If two fixes failed and both assumed X, the next action verifies X — not a third fix.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: principle-boundary-discipline
|
|
3
|
+
description: Guards at boundaries, pure functions inside. Apply when handling external input, I/O, or untrusted data.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Boundary Discipline
|
|
8
|
+
|
|
9
|
+
**Why:** Validation scattered through the interior means every function distrusts its input and none can be reasoned about alone. Check once at the edge; keep the inside pure.
|
|
10
|
+
|
|
11
|
+
- Validate, parse, and normalize at the system boundary — never deeper.
|
|
12
|
+
- Inside the boundary, functions assume well-formed input and stay pure where possible.
|
|
13
|
+
- Errors from outside become typed/domain errors at the boundary, not deep in logic.
|
|
14
|
+
- Never let raw external shapes leak past the first layer.
|
|
15
|
+
|
|
16
|
+
**Test:** Interior functions contain no defensive checks for malformed external input — the boundary already guaranteed shape.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: principle-encode-lessons-in-structure
|
|
3
|
+
description: Turn repeated lessons into lint rules, flags, or scripts — not more prose. Apply when the same mistake or instruction recurs.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Encode Lessons in Structure
|
|
8
|
+
|
|
9
|
+
**Why:** Prose instructions are re-read, re-forgotten, and re-violated. A rule encoded in a check, flag, or script enforces itself forever at zero attention cost.
|
|
10
|
+
|
|
11
|
+
- When the same correction appears twice, encode it: lint rule, test, flag, script, or template.
|
|
12
|
+
- Prefer a mechanism that fails loudly over documentation that advises quietly.
|
|
13
|
+
- Keep the prose pointer minimal — the structure is the lesson.
|
|
14
|
+
- Apply to process too: a repeated manual step is a script waiting to exist.
|
|
15
|
+
|
|
16
|
+
**Test:** The next occurrence of the mistake fails a check automatically instead of needing a reviewer to catch it.
|