@ngockhoale/ukit 2.7.12 → 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.
Files changed (46) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/manifests/documentation.yaml +11 -0
  3. package/manifests/platform.full.yaml +182 -0
  4. package/manifests/platform.user.yaml +53 -0
  5. package/package.json +3 -1
  6. package/src/cli/commands/diff.js +4 -2
  7. package/src/cli/commands/doctor.js +22 -1
  8. package/src/cli/commands/install.js +10 -0
  9. package/src/cli/commands/memory.js +142 -3
  10. package/src/cli/commands/playbook.js +53 -0
  11. package/src/cli/index.js +7 -0
  12. package/src/core/memory/recordStore.js +81 -0
  13. package/src/core/memory/storeV2.js +16 -52
  14. package/src/core/memory/userMemory.js +111 -0
  15. package/src/core/paths.js +1 -0
  16. package/src/core/runInstallPipeline.js +96 -3
  17. package/src/core/runtimeConfig.js +170 -5
  18. package/src/core/userPaths.js +21 -0
  19. package/src/core/userPlaybooks.js +185 -0
  20. package/src/index/taskRouting.js +422 -21
  21. package/src/index/verificationPlan.js +17 -0
  22. package/src/manifest/validateManifest.js +19 -0
  23. package/templates/.claude/config/providers.md +1 -3
  24. package/templates/.claude/skills/principle-attack-the-premise/SKILL.md +16 -0
  25. package/templates/.claude/skills/principle-boundary-discipline/SKILL.md +16 -0
  26. package/templates/.claude/skills/principle-encode-lessons-in-structure/SKILL.md +16 -0
  27. package/templates/.claude/skills/principle-fix-root-causes/SKILL.md +18 -0
  28. package/templates/.claude/skills/principle-foundational-thinking/SKILL.md +17 -0
  29. package/templates/.claude/skills/principle-guard-the-context-window/SKILL.md +16 -0
  30. package/templates/.claude/skills/principle-laziness-protocol/SKILL.md +17 -0
  31. package/templates/.claude/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +16 -0
  32. package/templates/.claude/skills/principle-minimize-reader-load/SKILL.md +17 -0
  33. package/templates/.claude/skills/principle-model-the-domain/SKILL.md +16 -0
  34. package/templates/.claude/skills/principle-never-block-on-the-human/SKILL.md +16 -0
  35. package/templates/.claude/skills/principle-prove-it-works/SKILL.md +18 -0
  36. package/templates/.claude/skills/principle-sequence-verifiable-units/SKILL.md +16 -0
  37. package/templates/.claude/skills/principle-subtract-before-you-add/SKILL.md +16 -0
  38. package/templates/.claude/skills/principle-test-behavior-not-implementation/SKILL.md +18 -0
  39. package/templates/.claude/ukit/index/route-task.mjs +652 -28
  40. package/templates/.claude/ukit/runtime/execution-ledger.mjs +238 -9
  41. package/templates/ukit/README.md +31 -0
  42. package/templates/ukit/storage/config.json +10 -0
  43. package/templates/user/README.md +21 -0
  44. package/templates/user/playbooks/bug-fix.md +18 -0
  45. package/templates/user/playbooks/issue-implementation.md +14 -0
  46. package/templates/user/storage/config.json +16 -0
@@ -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
- // A bare delivery command ("push this to git", "đẩy bộ này lên git") performs no
519
- // repository mutation the ledger could ever receipt. With no edit/review/debug/
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.