@ngockhoale/ukit 1.1.8 → 1.2.1

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 CHANGED
@@ -4,6 +4,17 @@ All notable changes to UKit are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.2.1 - 2026-05-05
8
+
9
+ ### Local AI Task Queue
10
+
11
+ - Added gitignored `docs/TASKS.md` as a local AI task queue for deferred work that one AI can write and another AI/agent can implement later.
12
+ - Installed `docs/TASKS.md` with skip merge semantics so end-user task notes are never overwritten or uninstall-owned.
13
+ - Added safe default cleanup rules: remove exact duplicates, prune `Done Recently` to 10 compact lines, move stale/vague work to deferred review, and never delete unfinished human-authored tasks unless explicitly asked and clearly obsolete/duplicated.
14
+ - Updated `next-step`, `update-status`, and `docs-quality` guidance plus route signals so queued-task selection goes to `next-step` while task cleanup/editing goes to `docs-quality`.
15
+ - Added `docs/STATUS.md` and `docs/TASKS.md` to generated gitignore handling so living local AI state does not pollute project commits.
16
+ - Added the internal `ukit-small-task-maintainer` subagent, powered by optional `UKIT_SMALL_TASK_MODEL=unic-lite`, for safe UKit decisions such as task cleanup, compact decisions, doc summarization, classification, auto-triage, queue maintenance, and small reversible cleanup while keeping risky/security/release work on the main model.
17
+
7
18
  ## 1.1.8 - 2026-05-05
8
19
 
9
20
  ### Living Project Status
package/README.md CHANGED
@@ -33,6 +33,7 @@ ukit install
33
33
  - `docs/PROJECT.md`
34
34
  - `docs/MEMORY.md`
35
35
  - `docs/STATUS.md`
36
+ - `docs/TASKS.md`
36
37
  - `docs/WORKLOG.md`
37
38
  5. Open your AI tool and work in natural language.
38
39
 
@@ -65,13 +66,13 @@ If maintainers roll out a newer CLI build, the in-project workflow still stays t
65
66
  **Project support files**
66
67
  - `.claude/ukit/.ukit/` — installer manifests, metadata, backups
67
68
  - `.ukit/` — hidden shared runtime storage for config, cache, and cross-agent memory
68
- - `docs/` — PROJECT / MEMORY / STATUS / WORKLOG baseline
69
+ - `docs/` — PROJECT / MEMORY / STATUS / TASKS / WORKLOG baseline
69
70
 
70
- ## UKit v1.1.8 Runtime
71
+ ## UKit v1.2.1 Runtime
71
72
 
72
73
  UKit now installs a hidden shared local runtime at `.ukit/` for production-oriented state that should survive across agent sessions:
73
74
 
74
- - `.ukit/storage/config.json` — runtime defaults for compact/router/memory/validation
75
+ - `.ukit/storage/config.json` — runtime defaults for compact/router/memory/validation/subagent hints
75
76
  - `.ukit/storage/cache/` — reusable prompt-cache, compact history, compact-pressure state, and output summaries
76
77
  - `.ukit/storage/memory/` — cross-agent local memory
77
78
 
@@ -84,17 +85,18 @@ When long sessions approach the compact threshold, UKit now uses a conservative
84
85
  - compact only safe-zone history/noise
85
86
  - preserve active task, rules, decisions, and current code focus
86
87
 
87
- UKit v1.1.8 keeps the same shared runtime contract while adding living project status routing for continuity-safe AI sessions:
88
+ UKit v1.2.1 keeps the same shared runtime contract while adding a local AI task queue alongside living project status routing:
88
89
 
89
90
  - install globally with `npm install -g @ngockhoale/ukit`
90
91
  - keep using the exact same human workflow inside projects: `ukit install`
91
92
  - preserve the same `ukit` binary, hooks, and install-first orchestration while standardizing the runtime root as hidden `.ukit/`
92
93
  - install `docs/STATUS.md` as a compact living state file for active work, debug threads, blockers, verification, and next candidates
94
+ - install gitignored `docs/TASKS.md` as a local AI task queue for deferred/ready work, with safe AI cleanup rules that remove duplicates and prune completed history without deleting unfinished human intent
93
95
  - auto-route open-ended “what next?” / “continue” prompts to the `next-step` skill with a visible freshness cue when status may be stale
94
96
  - auto-route explicit handoff/wrap-up requests to the `update-status` skill while skipping trivial/no-state-change tasks
95
97
  - keep concrete debug/implementation/review prompts primary, so project status never replaces source/index-first task work
96
98
 
97
- UKit v1.1.8 also keeps the fast path improvements from the recent runtime releases:
99
+ UKit v1.2.1 also keeps the fast path improvements from the recent runtime releases:
98
100
 
99
101
  - Vietnamese prompts now normalize more effectively for English-heavy code symbols and paths
100
102
  - localized simple direct-target lanes skip extra previous-context / recent-output work when it would not change the next action
@@ -129,6 +131,8 @@ UKit is built so the team can stop memorizing UKit subcommands and focus on prod
129
131
  - let Claude Code / Codex / OpenCode auto-detect and use the right project-local skill from the prompt and the files/tools involved
130
132
  - let the AI prefer targeted verification first, then widen only when shared/risky scope justifies it
131
133
  - let the AI selectively auto-delegate internal subagents only when that actually reduces context/noise or unlocks parallel progress; small localized work should stay direct
134
+ - keep long sessions compact across agents: Claude keeps PreCompact/reinject, OpenCode keeps native auto/prune compaction, and Codex Desktop uses internal `UKIT_CODEX_COMPACT_TARGET` soft handoffs (default 150 lines; 120-150 preferred, hard max 170), without asking end users to manage context manually
135
+ - let UKit internally use the `ukit-small-task-maintainer` subagent with `UKIT_SMALL_TASK_MODEL=unic-lite` for safe task cleanup, fast-vs-slow/safe-vs-risky lane hints, skill-routing/step-budget hints, agent context-budget decisions, compact decisions, doc summarization, classification, and queue maintenance while risky/security/release/quality-risk work stays on the main model
132
136
  - rerun `ukit install` when you need to refresh the workspace
133
137
 
134
138
  End users should **not** need to know or memorize skill names.
@@ -77,6 +77,18 @@ items:
77
77
  packs:
78
78
  - core
79
79
 
80
+ - id: ukit-env-example
81
+ type: config
82
+ sourceTemplate: .claude/ukit/.env.example
83
+ targetPath: .claude/ukit/.env.example
84
+ requires:
85
+ - runtime-config
86
+ mergeStrategy: overwrite_with_backup
87
+ variables: []
88
+ enabledByDefault: true
89
+ packs:
90
+ - core
91
+
80
92
  - id: root-claude-md
81
93
  type: config
82
94
  sourceTemplate: CLAUDE.md
@@ -159,6 +171,19 @@ items:
159
171
  packs:
160
172
  - core
161
173
 
174
+ - id: docs-tasks
175
+ type: config
176
+ sourceTemplate: docs/TASKS.md
177
+ targetPath: docs/TASKS.md
178
+ requires:
179
+ - docs-status
180
+ mergeStrategy: skip
181
+ variables:
182
+ - project.name
183
+ enabledByDefault: true
184
+ packs:
185
+ - core
186
+
162
187
  - id: docs-bugfix
163
188
  type: config
164
189
  sourceTemplate: docs/BUGFIX.md
@@ -483,6 +508,7 @@ items:
483
508
  requires:
484
509
  - docs-quality-skill
485
510
  - docs-status
511
+ - docs-tasks
486
512
  mergeStrategy: overwrite_with_backup
487
513
  variables: []
488
514
  enabledByDefault: true
@@ -496,6 +522,7 @@ items:
496
522
  requires:
497
523
  - docs-quality-skill
498
524
  - docs-status
525
+ - docs-tasks
499
526
  mergeStrategy: overwrite_with_backup
500
527
  variables: []
501
528
  enabledByDefault: true
@@ -792,6 +819,19 @@ items:
792
819
  packs:
793
820
  - core
794
821
 
822
+ - id: agent-ukit-small-task-maintainer
823
+ type: agent
824
+ sourceTemplate: .claude/agents/ukit-small-task-maintainer.md
825
+ targetPath: .claude/agents/ukit-small-task-maintainer.md
826
+ requires:
827
+ - ukit-env-example
828
+ mergeStrategy: overwrite_with_backup
829
+ variables:
830
+ - ukit.version
831
+ enabledByDefault: true
832
+ packs:
833
+ - core
834
+
795
835
  - id: provider-compat-config
796
836
  type: config
797
837
  sourceTemplate: .claude/config/providers.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "1.1.8",
3
+ "version": "1.2.1",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, Antigravity, OpenAI Codex, and OpenCode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -63,6 +63,7 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
63
63
  docsProjectExists: await pathExists(path.join(projectRoot, 'docs', 'PROJECT.md')),
64
64
  docsMemoryExists: await pathExists(path.join(projectRoot, 'docs', 'MEMORY.md')),
65
65
  docsStatusExists: await pathExists(path.join(projectRoot, 'docs', 'STATUS.md')),
66
+ docsTasksExists: await pathExists(path.join(projectRoot, 'docs', 'TASKS.md')),
66
67
  docsWorklogExists: await pathExists(path.join(projectRoot, 'docs', 'WORKLOG.md')),
67
68
  allProvidersConfigured: providers.allSupported,
68
69
  ...(codexAdapterTracked
@@ -105,6 +106,7 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
105
106
  console.log(`[UKit] ${ok(checks.docsProjectExists)} docs/PROJECT.md`);
106
107
  console.log(`[UKit] ${ok(checks.docsMemoryExists)} docs/MEMORY.md`);
107
108
  console.log(`[UKit] ${ok(checks.docsStatusExists)} docs/STATUS.md`);
109
+ console.log(`[UKit] ${ok(checks.docsTasksExists)} docs/TASKS.md`);
108
110
  console.log(`[UKit] ${ok(checks.docsWorklogExists)} docs/WORKLOG.md`);
109
111
  if (codexAdapterTracked) {
110
112
  console.log(`[UKit] ${ok(checks.codexReadmeExists)} .codex/README.md`);
@@ -241,13 +241,14 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
241
241
  .join(', ');
242
242
  console.log(`[UKit] Providers: ${providerStatus}, all=${result.providerContext.allSupported}`);
243
243
 
244
- const docsPaths = [
245
- path.join(projectRoot, 'docs', 'PROJECT.md'),
246
- path.join(projectRoot, 'docs', 'MEMORY.md'),
247
- path.join(projectRoot, 'docs', 'STATUS.md'),
248
- path.join(projectRoot, 'docs', 'WORKLOG.md'),
244
+ const docsLabels = [
245
+ 'docs/PROJECT.md',
246
+ 'docs/MEMORY.md',
247
+ 'docs/STATUS.md',
248
+ 'docs/TASKS.md',
249
+ 'docs/WORKLOG.md',
249
250
  ];
250
- const docsLabels = ['docs/PROJECT.md', 'docs/MEMORY.md', 'docs/STATUS.md', 'docs/WORKLOG.md'];
251
+ const docsPaths = docsLabels.map((label) => path.join(projectRoot, ...label.split('/')));
251
252
  const missingDocs = [];
252
253
  for (let i = 0; i < docsPaths.length; i++) {
253
254
  if (!(await pathExists(docsPaths[i]))) {
@@ -258,7 +259,7 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
258
259
  if (missingDocs.length > 0) {
259
260
  console.log(`[UKit] Missing docs — fill these in before first use: ${missingDocs.join(', ')}`);
260
261
  } else {
261
- console.log('[UKit] Docs baseline ready: docs/PROJECT.md, docs/MEMORY.md, docs/STATUS.md, docs/WORKLOG.md');
262
+ console.log('[UKit] Docs baseline ready: docs/PROJECT.md, docs/MEMORY.md, docs/STATUS.md, docs/TASKS.md, docs/WORKLOG.md');
262
263
  console.log('[UKit] Fill them once with real project context for the best results.');
263
264
  }
264
265
 
@@ -47,5 +47,5 @@ export async function runUninstall({ projectRoot, argv = [] }) {
47
47
  }
48
48
 
49
49
  console.log(`[UKit] Uninstall complete. Removed ${result.removed}/${result.attempted} managed paths.`);
50
- console.log('[UKit] Note: docs/PROJECT.md, docs/MEMORY.md, docs/STATUS.md, docs/WORKLOG.md contain user content and were preserved. Delete manually if needed.');
50
+ console.log('[UKit] Note: docs/PROJECT.md, docs/MEMORY.md, docs/STATUS.md, docs/TASKS.md, docs/WORKLOG.md contain user content and were preserved. Delete manually if needed.');
51
51
  }
@@ -11,6 +11,8 @@ const UKIT_ENTRIES = [
11
11
  'opencode.json',
12
12
  'AGENTS.md',
13
13
  'CLAUDE.md',
14
+ 'docs/STATUS.md',
15
+ 'docs/TASKS.md',
14
16
  '.claude/ukit/.ukit/',
15
17
  '.claude/ukit/permission-usage.json',
16
18
  '.claude/ukit/permission-audit.log',
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs/promises';
2
+ import path from 'node:path';
2
3
  import { buildRuntimePaths } from './runtimePaths.js';
3
4
 
4
5
  const VALID_AGENTS = new Set(['claude-code', 'codex', 'antigravity', 'opencode']);
@@ -38,17 +39,155 @@ function pushPositiveNumberError(errors, value, label) {
38
39
  }
39
40
  }
40
41
 
42
+ function pushNonEmptyStringError(errors, value, label) {
43
+ if (typeof value !== 'string' || value.trim() === '') {
44
+ errors.push(`${label} must be a non-empty string.`);
45
+ }
46
+ }
47
+
48
+ function parseRuntimeEnv(content = '') {
49
+ const result = {};
50
+ for (const rawLine of String(content || '').split(/\r?\n/)) {
51
+ const line = rawLine.trim();
52
+ if (!line || line.startsWith('#') || !line.includes('=')) {
53
+ continue;
54
+ }
55
+ const index = line.indexOf('=');
56
+ const key = line.slice(0, index).trim();
57
+ let value = line.slice(index + 1).trim();
58
+ if (
59
+ (value.startsWith('"') && value.endsWith('"'))
60
+ || (value.startsWith("'") && value.endsWith("'"))
61
+ ) {
62
+ value = value.slice(1, -1);
63
+ }
64
+ result[key] = value;
65
+ }
66
+ return result;
67
+ }
68
+
69
+ async function loadRuntimeEnv(projectRoot) {
70
+ const envPath = path.join(projectRoot, '.claude', 'ukit', '.env');
71
+ try {
72
+ return parseRuntimeEnv(await fs.readFile(envPath, 'utf8'));
73
+ } catch (error) {
74
+ if (error?.code !== 'ENOENT') {
75
+ throw error;
76
+ }
77
+ return {};
78
+ }
79
+ }
80
+
81
+ function parsePositiveIntegerEnv(value) {
82
+ if (typeof value !== 'string' || value.trim() === '') {
83
+ return null;
84
+ }
85
+ const parsed = Number.parseInt(value.trim(), 10);
86
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
87
+ }
88
+
89
+ function buildEnvOverrides(env = process.env) {
90
+ const overrides = {};
91
+ const smallTaskModel = env.UKIT_SMALL_TASK_MODEL;
92
+ if (typeof smallTaskModel === 'string' && smallTaskModel.trim() !== '') {
93
+ overrides.subagents = {
94
+ ...(overrides.subagents ?? {}),
95
+ smallTaskModel: smallTaskModel.trim(),
96
+ };
97
+ }
98
+
99
+ const rawCodexCompactTarget = parsePositiveIntegerEnv(env.UKIT_CODEX_COMPACT_TARGET);
100
+ const codexCompactTarget = rawCodexCompactTarget ? Math.min(rawCodexCompactTarget, 170) : null;
101
+ const codexContextBudget = parsePositiveIntegerEnv(env.UKIT_CODEX_CONTEXT_BUDGET);
102
+ const codexAutoCompact = typeof env.UKIT_CODEX_AUTO_COMPACT === 'string'
103
+ ? !['0', 'false', 'no', 'off'].includes(env.UKIT_CODEX_AUTO_COMPACT.trim().toLowerCase())
104
+ : null;
105
+
106
+ if (codexCompactTarget || codexContextBudget || codexAutoCompact !== null) {
107
+ overrides.compact = {
108
+ ...(overrides.compact ?? {}),
109
+ agentContext: {
110
+ enabled: true,
111
+ decisionModelEnv: 'UKIT_SMALL_TASK_MODEL',
112
+ decisionAgent: 'ukit-small-task-maintainer',
113
+ executionMode: 'sidecar-parallel',
114
+ mustNotBlockMainTask: true,
115
+ targets: {
116
+ claude: { autoCompact: true, mode: 'precompact-reinject', preserveExistingHooks: true },
117
+ opencode: { autoCompact: true, mode: 'native-auto-prune', preserveExistingCompaction: true },
118
+ codex: {
119
+ autoCompact: true,
120
+ mode: 'soft-handoff',
121
+ compactTarget: 150,
122
+ compactTargetUnit: 'lines',
123
+ compactTargetRecommendedRange: [120, 170],
124
+ compactTargetMax: 170,
125
+ },
126
+ },
127
+ },
128
+ codexContext: {
129
+ ...(overrides.compact?.codexContext ?? {}),
130
+ ...(codexCompactTarget ? { compactTarget: codexCompactTarget } : {}),
131
+ ...(codexContextBudget ? { budgetTokens: codexContextBudget } : {}),
132
+ ...(codexAutoCompact !== null ? { autoCompact: codexAutoCompact } : {}),
133
+ },
134
+ };
135
+ }
136
+
137
+ return overrides;
138
+ }
139
+
41
140
  export function buildDefaultRuntimeConfig(overrides = {}) {
42
141
  const safeOverrides = isPlainObject(overrides) ? overrides : {};
43
142
 
44
143
  return mergeObjects({
45
- version: '1.1.8',
144
+ version: '1.2.1',
46
145
  agent: 'claude-code',
47
146
  compact: {
48
147
  enabled: true,
49
148
  tokenThreshold: 100_000,
50
149
  contextRotDetection: true,
51
150
  askBeforeDrop: true,
151
+ agentContext: {
152
+ enabled: true,
153
+ decisionModelEnv: 'UKIT_SMALL_TASK_MODEL',
154
+ decisionAgent: 'ukit-small-task-maintainer',
155
+ executionMode: 'sidecar-parallel',
156
+ mustNotBlockMainTask: true,
157
+ targets: {
158
+ claude: { autoCompact: true, mode: 'precompact-reinject', preserveExistingHooks: true },
159
+ opencode: { autoCompact: true, mode: 'native-auto-prune', preserveExistingCompaction: true },
160
+ codex: {
161
+ autoCompact: true,
162
+ mode: 'soft-handoff',
163
+ compactTarget: 150,
164
+ compactTargetUnit: 'lines',
165
+ compactTargetRecommendedRange: [120, 170],
166
+ compactTargetMax: 170,
167
+ },
168
+ },
169
+ },
170
+ codexContext: {
171
+ enabled: true,
172
+ autoCompact: true,
173
+ budgetTokens: 60_000,
174
+ compactTarget: 150,
175
+ compactTargetUnit: 'lines',
176
+ compactTargetRecommendedRange: [120, 170],
177
+ compactTargetMax: 170,
178
+ decisionModelEnv: 'UKIT_SMALL_TASK_MODEL',
179
+ decisionAgent: 'ukit-small-task-maintainer',
180
+ mode: 'soft-handoff',
181
+ preserve: [
182
+ 'current-goal',
183
+ 'non-negotiable-rules',
184
+ 'active-files',
185
+ 'decisions',
186
+ 'unresolved-failures',
187
+ 'verification-evidence',
188
+ 'next-actions',
189
+ ],
190
+ },
52
191
  },
53
192
  tokenPipeline: {
54
193
  inputCompression: true,
@@ -77,6 +216,53 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
77
216
  maxRetries: 1,
78
217
  confidenceThreshold: 50,
79
218
  },
219
+ subagents: {
220
+ enabled: true,
221
+ smallTaskModel: 'unic-lite',
222
+ smallTaskAgent: 'ukit-small-task-maintainer',
223
+ smallTaskUseCases: [
224
+ 'task-cleanup',
225
+ 'compact-decision',
226
+ 'codex-context-budget',
227
+ 'doc-summarization',
228
+ 'classification',
229
+ 'small-decision',
230
+ 'auto-triage',
231
+ 'queue-maintenance',
232
+ 'workspace-maintenance',
233
+ ],
234
+ keepMainModelFor: [
235
+ 'security',
236
+ 'risky-code-change',
237
+ 'release',
238
+ 'data-loss',
239
+ 'architecture-decision',
240
+ 'deep-reasoning',
241
+ ],
242
+ decisionPolicy: {
243
+ nonBlocking: true,
244
+ endUserInvisible: true,
245
+ handBackOnRisk: true,
246
+ executionMode: 'sidecar-parallel',
247
+ mustNotBlockMainTask: true,
248
+ maxSidecarWaitMs: 0,
249
+ optimizeOrder: ['quality', 'safety', 'speed', 'token-discipline'],
250
+ decisions: [
251
+ 'fast-vs-slow-lane',
252
+ 'safe-vs-risky-lane',
253
+ 'skill-routing-needed',
254
+ 'step-budget-enough',
255
+ 'compact-now-or-later',
256
+ 'codex-context-budget',
257
+ 'summarize-docs-or-keep-detail',
258
+ ],
259
+ stepBudgets: {
260
+ trivial: { maxSteps: 1, verification: 'skip-unless-risky' },
261
+ simple: { maxSteps: 2, verification: 'targeted-if-covered' },
262
+ nonTrivial: { maxSteps: 4, verification: 'targeted-then-widen-on-risk' },
263
+ },
264
+ },
265
+ },
80
266
  }, safeOverrides);
81
267
  }
82
268
 
@@ -102,6 +288,40 @@ export function validateRuntimeConfig(config) {
102
288
  pushPositiveNumberError(errors, config.compact.tokenThreshold, 'compact.tokenThreshold');
103
289
  pushBooleanError(errors, config.compact.contextRotDetection, 'compact.contextRotDetection');
104
290
  pushBooleanError(errors, config.compact.askBeforeDrop, 'compact.askBeforeDrop');
291
+ if (!isPlainObject(config.compact.agentContext)) {
292
+ errors.push('compact.agentContext must be an object.');
293
+ } else {
294
+ pushBooleanError(errors, config.compact.agentContext.enabled, 'compact.agentContext.enabled');
295
+ pushNonEmptyStringError(errors, config.compact.agentContext.decisionModelEnv, 'compact.agentContext.decisionModelEnv');
296
+ pushNonEmptyStringError(errors, config.compact.agentContext.decisionAgent, 'compact.agentContext.decisionAgent');
297
+ pushNonEmptyStringError(errors, config.compact.agentContext.executionMode, 'compact.agentContext.executionMode');
298
+ pushBooleanError(errors, config.compact.agentContext.mustNotBlockMainTask, 'compact.agentContext.mustNotBlockMainTask');
299
+ if (!isPlainObject(config.compact.agentContext.targets)) {
300
+ errors.push('compact.agentContext.targets must be an object.');
301
+ }
302
+ }
303
+ if (!isPlainObject(config.compact.codexContext)) {
304
+ errors.push('compact.codexContext must be an object.');
305
+ } else {
306
+ pushBooleanError(errors, config.compact.codexContext.enabled, 'compact.codexContext.enabled');
307
+ pushBooleanError(errors, config.compact.codexContext.autoCompact, 'compact.codexContext.autoCompact');
308
+ pushPositiveNumberError(errors, config.compact.codexContext.budgetTokens, 'compact.codexContext.budgetTokens');
309
+ pushPositiveNumberError(errors, config.compact.codexContext.compactTarget, 'compact.codexContext.compactTarget');
310
+ pushNonEmptyStringError(errors, config.compact.codexContext.compactTargetUnit, 'compact.codexContext.compactTargetUnit');
311
+ if (!Array.isArray(config.compact.codexContext.compactTargetRecommendedRange)) {
312
+ errors.push('compact.codexContext.compactTargetRecommendedRange must be an array.');
313
+ }
314
+ pushPositiveNumberError(errors, config.compact.codexContext.compactTargetMax, 'compact.codexContext.compactTargetMax');
315
+ if (Number(config.compact.codexContext.compactTarget) > Number(config.compact.codexContext.compactTargetMax)) {
316
+ errors.push('compact.codexContext.compactTarget must be <= compact.codexContext.compactTargetMax.');
317
+ }
318
+ pushNonEmptyStringError(errors, config.compact.codexContext.decisionModelEnv, 'compact.codexContext.decisionModelEnv');
319
+ pushNonEmptyStringError(errors, config.compact.codexContext.decisionAgent, 'compact.codexContext.decisionAgent');
320
+ pushNonEmptyStringError(errors, config.compact.codexContext.mode, 'compact.codexContext.mode');
321
+ if (!Array.isArray(config.compact.codexContext.preserve)) {
322
+ errors.push('compact.codexContext.preserve must be an array.');
323
+ }
324
+ }
105
325
  }
106
326
 
107
327
  if (!isPlainObject(config.tokenPipeline)) {
@@ -147,6 +367,23 @@ export function validateRuntimeConfig(config) {
147
367
  pushPositiveNumberError(errors, config.validation.confidenceThreshold, 'validation.confidenceThreshold');
148
368
  }
149
369
 
370
+ if (!isPlainObject(config.subagents)) {
371
+ errors.push('subagents must be an object.');
372
+ } else {
373
+ pushBooleanError(errors, config.subagents.enabled, 'subagents.enabled');
374
+ pushNonEmptyStringError(errors, config.subagents.smallTaskModel, 'subagents.smallTaskModel');
375
+ pushNonEmptyStringError(errors, config.subagents.smallTaskAgent, 'subagents.smallTaskAgent');
376
+ if (!Array.isArray(config.subagents.smallTaskUseCases)) {
377
+ errors.push('subagents.smallTaskUseCases must be an array.');
378
+ }
379
+ if (!Array.isArray(config.subagents.keepMainModelFor)) {
380
+ errors.push('subagents.keepMainModelFor must be an array.');
381
+ }
382
+ if (!isPlainObject(config.subagents.decisionPolicy)) {
383
+ errors.push('subagents.decisionPolicy must be an object.');
384
+ }
385
+ }
386
+
150
387
  return {
151
388
  valid: errors.length === 0,
152
389
  errors,
@@ -170,16 +407,31 @@ export async function inspectRuntimeConfig(projectRoot) {
170
407
  }
171
408
  }
172
409
 
173
- const config = buildDefaultRuntimeConfig(rawConfig);
410
+ let runtimeEnv = {};
411
+ let envError = null;
412
+ try {
413
+ runtimeEnv = await loadRuntimeEnv(projectRoot);
414
+ } catch (error) {
415
+ envError = error?.message ?? String(error);
416
+ }
417
+
418
+ const config = buildDefaultRuntimeConfig(
419
+ mergeObjects(rawConfig ?? {}, buildEnvOverrides({ ...runtimeEnv, ...process.env })),
420
+ );
174
421
  const validation = validateRuntimeConfig(config);
175
- const errors = parseError ? [`config.json parse error: ${parseError}`, ...validation.errors] : validation.errors;
422
+ const errors = [
423
+ ...(parseError ? [`config.json parse error: ${parseError}`] : []),
424
+ ...(envError ? [`runtime .env error: ${envError}`] : []),
425
+ ...validation.errors,
426
+ ];
176
427
 
177
428
  return {
178
429
  exists,
179
430
  rawConfig,
431
+ runtimeEnv,
180
432
  config,
181
433
  parseError,
182
- valid: exists && !parseError && validation.valid,
434
+ valid: exists && !parseError && !envError && validation.valid,
183
435
  errors,
184
436
  };
185
437
  }
@@ -200,7 +200,7 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
200
200
  }
201
201
  } else {
202
202
  // Old format (no files list): fall back to hardcoded managed paths.
203
- // NOTE: docs/PROJECT.md, MEMORY.md, STATUS.md, WORKLOG.md are intentionally excluded.
203
+ // NOTE: docs/PROJECT.md, MEMORY.md, STATUS.md, TASKS.md, WORKLOG.md are intentionally excluded.
204
204
  // These are user-created content (mergeStrategy: skip) — deleting them
205
205
  // would cause data loss. Users must remove them manually if desired.
206
206
  const fallback = buildFallbackPaths(projectRoot);
@@ -223,8 +223,8 @@ export const ROUTE_CATALOG = [
223
223
  path: '.claude/skills/docs-quality/SKILL.md',
224
224
  order: 12,
225
225
  signals: [
226
- { type: 'prompt', regex: /\b(docs|documentation|readme|changelog|handoff|worklog|memory|code map|status\.md)\b/i, score: 4 },
227
- { type: 'file', regex: /\bdocs\/|readme\.md$|project\.md$|memory\.md$|status\.md$|worklog\.md$|code_map\.md$/i, score: 4 },
226
+ { type: 'prompt', regex: /\b(docs|documentation|readme|changelog|handoff|worklog|memory|code map|status\.md|tasks\.md|task queue|clean tasks|cleanup tasks|dọn tasks|dọn task)\b/i, score: 4 },
227
+ { type: 'file', regex: /\bdocs\/|readme\.md$|project\.md$|memory\.md$|status\.md$|tasks\.md$|worklog\.md$|code_map\.md$/i, score: 4 },
228
228
  ],
229
229
  },
230
230
  {
@@ -233,8 +233,8 @@ export const ROUTE_CATALOG = [
233
233
  order: 12.1,
234
234
  contextMode: 'standalone',
235
235
  signals: [
236
- { type: 'prompt', regex: /\b(what(?:'s| is)? next|next steps?|project status|current status|where are we|continue(?: from)?(?: last session)?|roadmap|status\.md)\b/i, score: 7 },
237
- { type: 'prompt', regex: /\b(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project)\b/i, score: 7 },
236
+ { type: 'prompt', regex: /\b(what(?:'s| is)? next|next steps?|project status|current status|where are we|continue(?: from)?(?: last session)?|roadmap|status\.md|task queue|tasks\.md|next queued task|pick next task|work from tasks)\b/i, score: 7 },
237
+ { type: 'prompt', regex: /\b(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project|task tiếp theo|việc tiếp theo trong tasks)\b/i, score: 7 },
238
238
  ],
239
239
  },
240
240
  {
@@ -175,6 +175,7 @@ export function buildRouteSummary({
175
175
  formatCompactSegment('targets', primaryTargets),
176
176
  formatCompactSegment('tests', relatedTests),
177
177
  formatCompactSegment('styles', styleFiles),
178
+ delegationRecommendation?.hint ? `delegate=${delegationRecommendation.hint}` : null,
178
179
  policyMode ? `policy=${policyMode}` : null,
179
180
  ].filter(Boolean).join(' | ');
180
181
 
@@ -311,10 +312,11 @@ function getSignalTexts(signalType, routeSignals = {}) {
311
312
  function deriveIntentMode({ promptText = '', commandText = '', targetFile = null } = {}) {
312
313
  const lower = buildRouteSignalText(promptText, commandText);
313
314
  const raw = `${promptText ?? ''}\n${commandText ?? ''}`.toLowerCase();
314
- const docsSpecific = hasDocsSpecificTaskSignal(lower, raw, targetFile);
315
+ const taskQueueNext = hasTaskQueueNextSignal(lower, raw);
316
+ const docsSpecific = hasDocsSpecificTaskSignal(lower, raw, targetFile, { taskQueueNext });
315
317
  const statusUpdate = hasStatusUpdateSignal(lower, raw);
316
- const openEndedStatus = hasOpenEndedStatusSignal(lower, raw);
317
- const concreteTask = hasConcreteTaskSignal(lower, raw, targetFile);
318
+ const openEndedStatus = hasOpenEndedStatusSignal(lower, raw) || taskQueueNext;
319
+ const concreteTask = hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext });
318
320
 
319
321
  if (docsSpecific) {
320
322
  return 'docs-specific';
@@ -356,13 +358,18 @@ function hasStatusUpdateSignal(lower, raw) {
356
358
  }
357
359
 
358
360
  function hasOpenEndedStatusSignal(lower, raw) {
359
- return /\b(what next|what is next|what's next|next step|next steps|project status|current status|where are we|continue|continue from last session|roadmap|status\.md)\b/.test(lower)
360
- || /\b(lam gi tiep|buoc tiep theo|tiep theo lam gi|lam tiep|dang o dau|trang thai project|tinh trang project)\b/.test(lower)
361
- || /\b(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project)\b/.test(raw);
361
+ return /\b(what next|what is next|what's next|next step|next steps|project status|current status|where are we|continue|continue from last session|roadmap|status\.md|task queue|tasks\.md|next queued task|pick next task|work from tasks)\b/.test(lower)
362
+ || /\b(lam gi tiep|buoc tiep theo|tiep theo lam gi|lam tiep|dang o dau|trang thai project|tinh trang project|task tiep theo|viec tiep theo trong tasks)\b/.test(lower)
363
+ || /\b(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project|task tiếp theo|việc tiếp theo trong tasks)\b/.test(raw);
362
364
  }
363
365
 
364
- function hasConcreteTaskSignal(lower, raw, targetFile) {
365
- if (targetFile && !isStatusFileTarget(targetFile)) {
366
+ function hasTaskQueueNextSignal(lower, raw) {
367
+ return /\b(task queue|tasks\.md|next queued task|pick next task|work from tasks|next task from tasks|ready for ai)\b/.test(lower)
368
+ || /\b(task tiếp theo|việc tiếp theo trong tasks|làm task trong tasks|lấy task tiếp theo)\b/.test(raw);
369
+ }
370
+
371
+ function hasConcreteTaskSignal(lower, raw, targetFile, { taskQueueNext = false } = {}) {
372
+ if (targetFile && !isStatusFileTarget(targetFile) && !(taskQueueNext && isTasksFileTarget(targetFile))) {
366
373
  return true;
367
374
  }
368
375
 
@@ -370,8 +377,13 @@ function hasConcreteTaskSignal(lower, raw, targetFile) {
370
377
  || /\b(sửa|fix|lỗi|bug|debug|implement|cài|thêm|review|kiểm tra|soát|đăng nhập)\b/.test(raw);
371
378
  }
372
379
 
373
- function hasDocsSpecificTaskSignal(lower, raw, targetFile) {
380
+ function hasDocsSpecificTaskSignal(lower, raw, targetFile, { taskQueueNext = false } = {}) {
374
381
  if (!targetFile || !isDocsTarget(targetFile)) {
382
+ return /\b(clean tasks|cleanup tasks|prune tasks|dedupe tasks|clear completed tasks|dọn tasks|dọn task)\b/.test(lower)
383
+ || /\b(dọn tasks|dọn task|dọn danh sách task|xóa task đã xong)\b/.test(raw);
384
+ }
385
+
386
+ if (taskQueueNext && isTasksFileTarget(targetFile) && !/\b(clean|cleanup|prune|dedupe|edit|template|wording|format|structure)\b/.test(lower)) {
375
387
  return false;
376
388
  }
377
389
 
@@ -413,6 +425,10 @@ function isStatusFileTarget(targetFile) {
413
425
  return /(?:^|\/)docs\/STATUS\.md$|(?:^|\/)STATUS\.md$/i.test(String(targetFile || ''));
414
426
  }
415
427
 
428
+ function isTasksFileTarget(targetFile) {
429
+ return /(?:^|\/)docs\/TASKS\.md$|(?:^|\/)TASKS\.md$/i.test(String(targetFile || ''));
430
+ }
431
+
416
432
  function isDocsTarget(targetFile) {
417
433
  const normalized = String(targetFile || '').replaceAll('\\', '/');
418
434
  return /(?:^|\/)docs\/.+\.md$|(?:^|\/)(?:README|CHANGELOG|AGENTS|CLAUDE|STATUS)\.md$/i.test(normalized);
@@ -589,6 +605,16 @@ function deriveDelegationRecommendation({
589
605
  const hasRelatedTests = (preview.relatedTests ?? []).length > 0;
590
606
  const when = contextRecommendation?.command ? 'after-context' : 'now';
591
607
 
608
+ const smallTaskMaintenanceSignal = /\b(?:ukit|docs\/tasks\.md|tasks\.md|task queue|queued tasks?|compact(?:ion)?|summari[sz](?:e|ation)|doc(?:ument)? summary|cleanup|clean up|dọn rác|dọn dẹp|auto[- ]?triage|queue maintenance|small decision|ra quyết định)\b/.test(lower)
609
+ && /\b(?:task|queue|compact(?:ion)?|summari[sz](?:e|ation)|doc(?:ument)?s?|cleanup|clean up|dọn rác|dọn dẹp|classif(?:y|ication)|triage|maintenance|small decision|ra quyết định)\b/.test(lower);
610
+ if (smallTaskMaintenanceSignal) {
611
+ return {
612
+ hint: 'ukit-small-task-maintainer',
613
+ when,
614
+ reason: 'Low-risk UKit maintenance decisions should use the configured small-task model without blocking the main AI flow.',
615
+ };
616
+ }
617
+
592
618
  if (routingContext.taskType === 'trivial') {
593
619
  return null;
594
620
  }