@ngockhoale/ukit 3.4.9 → 3.4.11

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
@@ -2,9 +2,18 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.4.10 - 2026-10-01
6
+
7
+ **sensitive-data-guard reads .env placeholders.** `classifySecretFile` now
8
+ exempts every dotenv name carrying a distributable marker
9
+ (`example|examples|sample|template|dist`) — `.env.example.local`,
10
+ `.env.production.example`, `.env.sample`, `.env.dist`, `.env.template`
11
+ alongside the old `.env.example` — for Read/Grep and Bash lanes alike.
12
+ Real env files (`.env`, `.env.local`, `.env.production`) stay blocked.
13
+
5
14
  ## 3.4.8 - 2026-10-01
6
15
 
7
- (Also republishes 3.4.7 — npm staged-version E409 limbo.)
16
+ (3.4.7 and 3.4.9 were lost to npm staged-version E409 limbo; 3.4.8 landed and its `latest` tag was assigned via `npm dist-tag`.)
8
17
 
9
18
 
10
19
  **Completion gate no longer demands an edit for read-only questions.** The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.4.9",
3
+ "version": "3.4.11",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -201,7 +201,14 @@ async function evaluate({ rawInput, projectRoot, env }) {
201
201
  const rawBase = normalized.split('/').pop();
202
202
  if (!rawBase) return null;
203
203
  const base = rawBase.toLowerCase();
204
- if (base.endsWith('.env.example')) return null;
204
+ // .env* placeholders that carry no secrets by convention: any dotenv file
205
+ // whose basename marks it a non-secret distributable — .env.example,
206
+ // .env.example.local, .env.examples, .env.sample, .env.dist,
207
+ // .env.template, .env.dev.example. The marker must appear after the
208
+ // '.env' prefix so a real env file can never dodge by containing the word
209
+ // earlier (there is nothing before '.env' in the basename anyway — the
210
+ // check is anchored on the dotenv name itself).
211
+ if (/^\.env[._-]?.*\b(example|examples|sample|template|dist)\b/i.test(base)) return null;
205
212
  if (base === '.env' || base.startsWith('.env.')) return 'dotenv/secret env file';
206
213
  if (base.endsWith('.pub') || base.endsWith('.public')) return null;
207
214
  if (/^id_(rsa|dsa|ecdsa|ed25519)(\.|$)/.test(base)) return 'SSH private key';
@@ -2157,6 +2157,32 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
2157
2157
  return /(?<![A-Za-z0-9_])(?:implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|sua|them|tao|xoa|doi|thay\s+the|cap\s+nhat|viet|chay|cai|chinh|trien\s+khai|cau\s+hinh)(?![A-Za-z0-9_])/.test(residue);
2158
2158
  }
2159
2159
 
2160
+ // USER-REPORTED (2026-10-01): "chỉ lên plan cho tôi" / "just plan it" asks
2161
+ // for a plan and nothing more — the deliverable is a document in the reply,
2162
+ // not a mutation. mutationOrder=false alone does NOT cover these: they name
2163
+ // files and future work, so the router lands them on local-build/local-fix,
2164
+ // mints write debt, and the gate forces an implement the user never asked
2165
+ // for. Plan-only = explicit plan-request signal AND no surviving mutation
2166
+ // order ("lên plan rồi sửa luôn" is an implement request, not plan-only).
2167
+ function isPlanOnlyRequest({ promptText = '', commandText = '' } = {}) {
2168
+ const folded = `${promptText ?? ''}\n${commandText ?? ''}`
2169
+ .toLowerCase()
2170
+ .normalize('NFD')
2171
+ .replace(/[\u0300-\u036f]/g, '')
2172
+ .replace(/\u0111/g, 'd');
2173
+ const trimmed = folded.trim();
2174
+ if (!trimmed) return false;
2175
+ const planSignal =
2176
+ /\b(?:chi|just|only)\s+(?:len|lap|vach|lam)\b[^.,;\n]{0,40}\b(?:plan|ke\s+hoach|phuong\s+an|lo\s+trinh|roadmap|blueprint)\b/.test(trimmed)
2177
+ || /\b(?:len|lap|vach|de\s+xuat|draft)\s+(?:ke\s+hoach|plan|phuong\s+an|lo\s+trinh|roadmap)\b/.test(trimmed)
2178
+ || /\b(?:plan|planning)\s+(?:only|first|mode)\b/.test(trimmed)
2179
+ || /\b(?:ke\s+hoach|plan|phuong\s+an|lo\s+trinh|roadmap)\b[^.,;\n]{0,40}\b(?:khong|chua|khong\s+can)\s+(?:implement|code|thuc\s+hien|thuc\s+thi|sua|lam|build|viet)\b/.test(trimmed)
2180
+ || (/\b(?:khong|chua|dung)\s+(?:implement|code|thuc\s+hien|thuc\s+thi|sua|lam|viet\s+code)\b/.test(trimmed)
2181
+ && /\b(?:plan|ke\s+hoach|phuong\s+an|roadmap)\b/.test(trimmed));
2182
+ if (!planSignal) return false;
2183
+ return !hasMutationOrder({ promptText, commandText });
2184
+ }
2185
+
2160
2186
  function deriveExecutionMode({
2161
2187
  promptText = '',
2162
2188
  commandText = '',
@@ -2844,7 +2870,9 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
2844
2870
  ...(typeof routingContext.mutationOrder === 'boolean'
2845
2871
  ? { mutationOrder: routingContext.mutationOrder }
2846
2872
  : {}),
2847
- };
2873
+ ...(typeof routingContext.planOnly === 'boolean'
2874
+ ? { planOnly: routingContext.planOnly }
2875
+ : {}),
2848
2876
  }
2849
2877
 
2850
2878
  function compactHelpers({
@@ -3518,6 +3546,7 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
3518
3546
  // mutation so the Stop gate can release a misrouted write-debt instead
3519
3547
  // of looping "make an Edit" on an answer-only question.
3520
3548
  mutationOrder: promptText.trim() ? hasMutationOrder({ promptText, commandText }) : null,
3549
+ planOnly: promptText.trim() ? isPlanOnlyRequest({ promptText, commandText }) : null,
3521
3550
  };
3522
3551
  const previousContext = await buildPreviousContextSnapshot({
3523
3552
  projectRoot,
@@ -3615,6 +3644,9 @@ setTimeout(() => process.exit(0), HOOK_DEADLINE_MS).unref();
3615
3644
  // Was this prompt a real mutation order? The Stop gate reads this to
3616
3645
  // release read-only stops that were misrouted into a write-debt mode.
3617
3646
  mutationOrder: hasMutationOrder({ promptText, commandText }),
3647
+ // User-requested default-answer posture: a prompt that only wants a
3648
+ // plan/answer releases the gate even on a mutating route.
3649
+ planOnly: isPlanOnlyRequest({ promptText, commandText }),
3618
3650
  };
3619
3651
  const useIndexedContext = shouldUseIndexedContext({
3620
3652
  activeSkills: selected,
@@ -326,12 +326,15 @@ function hasUnfinishedCompletion(state = {}, ledger = {}) {
326
326
  if (!IMPLEMENT_MODES.has(mode)) return false;
327
327
  const required = requiredEvidence(state);
328
328
  if (required.length === 0) return false;
329
- // Read-only prompts misrouted into a mutating mode stamp
330
- // routingContext.mutationOrder === false; their write-debt is a misroute, not
331
- // unfinished work (see evaluateCompletion's 'no-mutation-order' valve).
332
- const effectiveRequired = state?.routingContext?.mutationOrder === false
333
- && ledger?.writeAttempted !== true
334
- ? required.filter((item) => item !== 'write-evidence')
329
+ // USER-REPORTED (2026-10-01): default posture is answer/plan/review — a
330
+ // prompt that never ordered a mutation, or an explicit plan-only request,
331
+ // must not mint unfinished write/verification debt even when it was
332
+ // misrouted into a mutating mode (see evaluateCompletion's
333
+ // 'no-mutation-order' valve). A real edit attempt keeps the debt.
334
+ const readOnlyPrompt = state?.routingContext?.mutationOrder === false
335
+ || state?.routingContext?.planOnly === true;
336
+ const effectiveRequired = readOnlyPrompt && ledger?.writeAttempted !== true
337
+ ? required.filter((item) => item !== 'write-evidence' && item !== 'verification-evidence')
335
338
  : required;
336
339
  const unfinished = effectiveRequired.some((item) => !evidenceSatisfied(item, ledger, state));
337
340
  if (unfinished) return true;
@@ -2637,30 +2640,29 @@ export function evaluateCompletion({ state = {}, ledger = {}, cwd } = {}) {
2637
2640
  };
2638
2641
  }
2639
2642
 
2640
- // USER-REPORTED (2026-10-01): a read-only question can still be routed into a
2641
- // mutating mode (find-cause/local-fix) when the prompt mentions mutation or
2642
- // failure vocabulary as SUBJECT matter ("nút Excel gọi hàm nào?", "find the
2643
- // function that runs on click") — the router's question-shape heuristics
2644
- // miss it, and the gate then demands a write the turn can never honestly
2645
- // produce: Stop → "make an Edit" → AskUserQuestion → Stop. The router now
2646
- // stamps routingContext.mutationOrder (hasMutationOrder on the raw prompt);
2647
- // `false` means the request never ordered a mutation, so write-debt is a
2648
- // misroute, not unfinished work. `null`/absent keeps the pre-stamp
2649
- // conservative path (no release) — only a positively-computed false opens
2650
- // the valve, and only when no mutation was actually attempted (a real edit
2651
- // keeps the normal gate regardless of prompt wording).
2652
- const mutationOrder = state?.routingContext?.mutationOrder;
2643
+ // USER-REPORTED (2026-10-01): the default posture is answer/plan/review —
2644
+ // "chỉ lên plan", "trả lời cho tôi", "xem lại plan" carry no mutation order
2645
+ // and the turn's deliverable is the reply itself. Two stamped signals cover
2646
+ // it: routingContext.mutationOrder === false (no imperative verb survived
2647
+ // the clause strips) or routingContext.planOnly === true (explicit
2648
+ // plan-request wording). When either holds, write-evidence AND
2649
+ // verification-evidence debt from a misrouted mutating lane are
2650
+ // misroutes, not unfinished work — release loud so the stop names why it
2651
+ // is allowed. `null`/absent stays conservative; an actual edit attempt
2652
+ // (writeAttempted) keeps the normal gate regardless of prompt wording.
2653
+ const readOnlyPrompt = state?.routingContext?.mutationOrder === false
2654
+ || state?.routingContext?.planOnly === true;
2653
2655
  if (
2654
- mutationOrder === false
2655
- && missingEvidence.includes('write-evidence')
2656
+ readOnlyPrompt
2656
2657
  && !effectiveLedger.writeAttempted
2658
+ && missingEvidence.some((item) => item === 'write-evidence' || item === 'verification-evidence')
2657
2659
  ) {
2658
2660
  return {
2659
2661
  continue: false,
2660
2662
  notify: true,
2661
2663
  gateRelease: 'no-mutation-order',
2662
2664
  missingEvidence,
2663
- reason: 'UKit completion gate released a read-only stop: this prompt ordered no mutation, so missing write-evidence is a misroute, not unfinished work. If an edit IS needed, re-send the task as an explicit change request.',
2665
+ reason: 'UKit completion gate released a read-only stop: this prompt asked for an answer or a plan, not a change, so missing write/verification evidence is a misroute, not unfinished work. To implement, re-send the task as an explicit change request ("implement", "sửa", "làm luôn", "execute this plan").',
2664
2666
  };
2665
2667
  }
2666
2668