@pugi/cli 0.1.0-beta.87 → 0.1.0-beta.88

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 (37) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +1 -1
  3. package/dist/core/agents/registry.js +1 -1
  4. package/dist/core/checkpoints/shadow-git.js +1 -1
  5. package/dist/core/context/compaction.js +1 -1
  6. package/dist/core/denial-tracking/state.js +1 -1
  7. package/dist/core/edits/fuzzy-ladder.js +1 -1
  8. package/dist/core/edits/layer-a-fuzzy-apply.js +1 -1
  9. package/dist/core/engine/anvil-client.js +13 -2
  10. package/dist/core/mcp/server-tools.js +1 -1
  11. package/dist/core/mcp/server.js +1 -1
  12. package/dist/core/memory/secret-scanner.js +6 -6
  13. package/dist/core/onboarding/ensure-initialized.js +1 -1
  14. package/dist/core/plans/plan-artifact.js +2 -2
  15. package/dist/core/repl/cap-warning.js +1 -1
  16. package/dist/core/routing/pre-flight-estimator.js +1 -1
  17. package/dist/core/settings.js +12 -0
  18. package/dist/index.js +8 -0
  19. package/dist/runtime/cli.js +68 -20
  20. package/dist/runtime/commands/config.js +41 -7
  21. package/dist/runtime/sigint-guard.js +272 -0
  22. package/dist/runtime/version.js +1 -1
  23. package/dist/skills/bundled/batch.js +2 -2
  24. package/dist/skills/bundled/index.js +3 -3
  25. package/dist/skills/bundled/loop.js +2 -2
  26. package/dist/skills/bundled/remember.js +1 -1
  27. package/dist/skills/bundled/simplify.js +1 -1
  28. package/dist/skills/bundled/skillify.js +2 -2
  29. package/dist/skills/bundled/stuck.js +1 -1
  30. package/dist/skills/bundled/verify.js +2 -2
  31. package/dist/testing/vcr.js +2 -2
  32. package/dist/tools/ask-user-question.js +66 -0
  33. package/dist/tools/bash.js +2 -2
  34. package/dist/tools/powershell.js +1 -1
  35. package/dist/tui/ask-user-question-chips.js +257 -0
  36. package/dist/tui/welcome-data.js +4 -4
  37. package/package.json +5 -4
package/CHANGELOG.md CHANGED
@@ -7,6 +7,42 @@ releases listed first. Section format: `## [<version>] - <YYYY-MM-DD>`.
7
7
  The bundled `pugi release-notes` command parses this file and renders sections
8
8
  strictly newer than `~/.pugi/.last-seen-version` after every upgrade.
9
9
 
10
+ ## [0.1.0-beta.88] - 2026-06-02
11
+
12
+ ### Fixed
13
+ - **Pugi identity intro no longer chants on later turns**. The output gate now
14
+ strips the canonical long-form intro ("I'm Pugi - your engineering copilot.
15
+ Tell me what you need...") and its Russian variant on mid-thread replies.
16
+ Operators were seeing the intro re-emit on turn 2+ after a session-resume
17
+ or autonomous tick.
18
+ - **Ctrl+C double-press exit guard**. A single Ctrl+C in the REPL no longer
19
+ kills the CLI. First press prompts "Press Ctrl+C again to exit (within 2s)
20
+ or any key to continue". Second press within 2s exits cleanly; any other
21
+ key cancels. Headless mode emits a `session-end` envelope and exits 0.
22
+ - **Persona no longer over-clarifies trivial creative tasks**. Asking for a
23
+ well-known game / demo / todo app now picks reasonable defaults and starts
24
+ building in one turn. Ambiguous tasks (auth, deploy targets) still ask
25
+ ≤ 3 short choices via the `ask_user_question` tool.
26
+
27
+ ### Added
28
+ - **Short-format AskUserQuestion chip renderer**. Up to 3 questions render
29
+ side-by-side as Ink chips with ▸ default highlight, ↑↓ in-question nav,
30
+ ←→/Tab between questions, 1-9 jump, [s] skip-with-default, [Esc] cancel,
31
+ [Enter] commit. Labels truncate at 5 words / 22 chars. Caller can opt
32
+ into a non-TTY numbered fallback via `forceFallback`.
33
+
34
+ ### Security
35
+ - **npm publish leak-gate hardening**. `prepublishOnly` now runs a
36
+ banned-string scan against the staged tarball (`tools/scrub/scan-tarball.sh`)
37
+ covering personal names, competitor refs, internal codenames, engineering
38
+ provenance, absolute paths, brand legacy, and secret-token shapes.
39
+ Per-line `[pugi-leak-ok]` allowlist marker for legitimate references;
40
+ path allowlist for LICENSE / THIRD_PARTY_NOTICES files where MIT requires
41
+ copyright-holder name. Synthetic injection regression test runs in CI.
42
+
43
+ ### Chore
44
+ - MIT LICENSE copyright holder set to `Pugi.io` (was personal name).
45
+
10
46
  ## [0.1.0-beta.26] - 2026-05-27
11
47
 
12
48
  ### Added
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Yurii Bulakh
3
+ Copyright (c) 2026 Pugi.io
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -30,7 +30,7 @@ function requirePersona(slug) {
30
30
  /**
31
31
  * CLI-only role-to-persona mapping. Roles are dispatcher-facing strings;
32
32
  * personas come from the brand-canonical THE_TEN. Vera (qa) intentionally
33
- * dual-roles as verifier + reviewer per ADR-0056 — the cabinet's review
33
+ * dual-roles as verifier + reviewer per — the cabinet's review
34
34
  * pipeline already merges the two surfaces.
35
35
  */
36
36
  export const SUBAGENT_REGISTRY = [
@@ -2,7 +2,7 @@
2
2
  * Per-task shadow git repo — file-state checkpoint surface.
3
3
  *
4
4
  * Inspired by Cline `CheckpointTracker.ts` (Apache-2.0).
5
- * Clean-room TypeScript implementation following Pugi conventions.
5
+ * independent implementation TypeScript implementation following Pugi conventions.
6
6
  *
7
7
  * Goal: every Pugi-orchestrated file mutation lands in a per-task
8
8
  * shadow git history kept entirely separate from the user's real
@@ -2,7 +2,7 @@
2
2
  * Six-tier context compaction engine for the Pugi CLI agent loop.
3
3
  *
4
4
  * Spec: `docs/research/pugi-cli-corpus/patterns/context-compaction.md`,
5
- * sprint slot: ADR-0056 §.
5
+ * sprint slot: §.
6
6
  *
7
7
  * Tiers and triggers (selectTier rules):
8
8
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * L11 — DenialTrackingState surface (the upstream tool parity).
3
3
  *
4
- * Per the upstream / anarchic-CC the upstream behavior (``
4
+ * Per the upstream / the upstream behavior (``
5
5
  * §5.2): the upstream tool's `QueryEngine.ts` maintains a per-session
6
6
  * `DenialTrackingState` that records every tool-dispatch denial.
7
7
  * Subsequent turns receive a compact reminder so the model does not
@@ -30,7 +30,7 @@
30
30
  * >= 0.8 to count as a match; below that we fail LOUD rather
31
31
  * than write the wrong region.
32
32
  *
33
- * Inspired by Aider editblock_coder.py (Apache-2.0). Clean-room
33
+ * Inspired by Aider editblock_coder.py (Apache-2.0). independent implementation
34
34
  * implementation; no Aider source code copied.
35
35
  *
36
36
  * Pure functions throughout — every tier returns a structured result
@@ -19,7 +19,7 @@
19
19
  * - `identical_replacement` — search and replace are identical;
20
20
  * would no-op; surfaced LOUD.
21
21
  *
22
- * Inspired by Aider editblock_coder.py (Apache-2.0). Clean-room
22
+ * Inspired by Aider editblock_coder.py (Apache-2.0). independent implementation
23
23
  * implementation; no Aider source code copied.
24
24
  */
25
25
  import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs';
@@ -64,11 +64,22 @@ export class AnvilEngineLoopClient {
64
64
  // PR-CLI-SERVER-VERSION-HANDSHAKE . Stamp the outbound
65
65
  // X-Pugi-Cli-Version header so the admin-api middleware can
66
66
  // decide whether to honour, soft-warn, or 426 this request.
67
- const outboundHeaders = injectClientVersionHeader({
67
+ // PUGI-260: also stamp `X-Pugi-Context-Tier: 1m` when the
68
+ // operator opted into the long-context lane. The server reads
69
+ // either the body's `contextTier` field OR this header (header is
70
+ // a fallback for older runtimes / non-CLI clients), so emitting
71
+ // both is harmless и belt-and-suspenders. Only emitted for the
72
+ // `'1m'` value — the absence of the header is wire-equivalent к
73
+ // `'standard'`, keeping the default-lane path header-free.
74
+ const baseHeaders = {
68
75
  'content-type': 'application/json',
69
76
  authorization: `Bearer ${this.config.apiKey}`,
70
77
  'user-agent': 'pugi-cli/0.0.1',
71
- }, PUGI_CLI_VERSION);
78
+ };
79
+ if (options.contextTier === '1m') {
80
+ baseHeaders['x-pugi-context-tier'] = '1m';
81
+ }
82
+ const outboundHeaders = injectClientVersionHeader(baseHeaders, PUGI_CLI_VERSION);
72
83
  const res = await fetch(url, {
73
84
  method: 'POST',
74
85
  headers: outboundHeaders,
@@ -6,7 +6,7 @@ import { bashToolSync } from '../../tools/bash.js';
6
6
  * The shapes intentionally mirror the engine-loop tool schemas in
7
7
  * `core/engine/tool-bridge.ts` so an MCP client and the Pugi engine see
8
8
  * the same parameter contracts. This is the "Pugi as MCP server"
9
- * surface — other agents (the upstream tool, Codex, OpenCode) call these to
9
+ * surface — other agents (the upstream tool, Codex, peer tooling) call these to
10
10
  * read / mutate the workspace through us, with all our security gates
11
11
  * (path containment, plan-mode refusal, bash classifier, settings) in
12
12
  * the loop.
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  /**
3
3
  * Pugi MCP server (β4 M2) — exposes Pugi's native tool surface to other
4
- * agents (the upstream tool, OpenCode, Codex CLI, any client that speaks
4
+ * agents (the upstream tool, peer tooling, Codex CLI, any client that speaks
5
5
  * MCP).
6
6
  *
7
7
  * Transport-agnostic core. The stdio entry-point lives at the bottom of
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Persona-memory secret scanner (backlog, ADR-0063 hardening).
2
+ * Persona-memory secret scanner (backlog, hardening).
3
3
  *
4
4
  * Defense against API keys / credentials accidentally landing in shared
5
5
  * persona memory. A naive operator typing `pugi memory write fact "Use
6
6
  * API key sk-ant-..."` would silently persist that secret to the
7
- * `cf_admin.persona_memory` table — visible to every persona, every
7
+ * `the platform database.persona_memory` table — visible to every persona, every
8
8
  * recall query, every dual-write sink. This module is the chokepoint
9
9
  * that refuses such writes.
10
10
  *
@@ -25,7 +25,7 @@
25
25
  * 3. GitHub PAT / installation tokens (ghp_/ghs_/gho_/ghu_)
26
26
  * high
27
27
  * 4. AWS access key id AKIA… high
28
- * 5. Plane API token plane_api_… high
28
+ * 5. Plane API token plane_api_… high [pugi-leak-ok]
29
29
  * 6. npm token npm_… high
30
30
  * 7. Slack token xox[bpoars]-… high
31
31
  * 8. Stripe secret key sk_(live|test)_… high
@@ -59,10 +59,10 @@
59
59
  * instead of reject. The scanner exposes `redactSecrets` for that
60
60
  * caller — see `runtime/commands/memory.ts`.
61
61
  *
62
- * # Clean-room provenance
62
+ * # independent implementation provenance
63
63
  *
64
64
  * Inspired by the the upstream tool teamMemorySync.secretScanner pattern
65
- * (intel from leak-research memos). Clean-room TypeScript
65
+ * (intel from leak-research memos). independent implementation TypeScript
66
66
  * implementation — no upstream code reused. Pattern vocabulary was
67
67
  * cross-referenced against the existing
68
68
  * `apps/pugi-cli/scripts/secret-scanner.mjs` tarball gate so a single
@@ -124,7 +124,7 @@ const SECRET_RULES = [
124
124
  pattern: 'plane-api-token',
125
125
  // Plane (project management) personal API tokens. Bounded to 20+
126
126
  // url-safe chars after the prefix.
127
- regex: /\bplane_api_[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
127
+ regex: /\bplane_api_[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g, // [pugi-leak-ok]
128
128
  confidence: 'high',
129
129
  },
130
130
  {
@@ -119,7 +119,7 @@ export async function ensureInitialized(opts) {
119
119
  write(`No Pugi workspace found at ${root}.\n`);
120
120
  const answer = (await opts.prompt('Initialize a new Pugi workspace here? (Y/n) ')).trim().toLowerCase();
121
121
  // Default = yes (empty input OR leading 'y'). Anything else = no.
122
- // Mirrors the gh CLI / claude code prompt convention where the upper-
122
+ // Mirrors the gh CLI / the upstream prompt convention where the upper-
123
123
  // case option in `(Y/n)` is the default-on-Enter answer.
124
124
  const acceptedShort = answer === '' || answer === 'y' || answer === 'yes';
125
125
  if (!acceptedShort) {
@@ -2,7 +2,7 @@
2
2
  * Plan-as-FILE artifact store (Pugi backlog).
3
3
  *
4
4
  * Pattern absorbed from the the upstream tool `ExitPlanMode` leak intel
5
- * (clean-room TypeScript reimplementation — no source was copied; only
5
+ * (independent implementation TypeScript reimplementation — no source was copied; only
6
6
  * the file-as-artifact concept). When Pugi enters plan-mode the engine
7
7
  * routes the plan body to `.pugi/plans/<plan-id>.md` instead of the
8
8
  * message stream so it survives `/compact`, becomes diffable across
@@ -205,7 +205,7 @@ function yamlScalar(value) {
205
205
  // Empty string is also disambiguated by quoting.
206
206
  if (value.length === 0)
207
207
  return '""';
208
- if (/[-"]/.test(value)) {
208
+ if (/[-"]/.test(value)) {
209
209
  return JSON.stringify(value);
210
210
  }
211
211
  if (/[:#\n\t]/.test(value)) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Client-side concurrent-subagent cap - Sprint (ADR-0056
2
+ * Client-side concurrent-subagent cap - Sprint (
3
3
  * acceptance #6, Mac safety memo
4
4
  * `feedback_max_3_parallel_agents_mac_safety.md`).
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pre-flight token estimator — Nuekkis tokenEstimation port,
2
+ * Pre-flight token estimator — external tokenEstimation port,
3
3
  * adapted for Anvil's 3-tier routing.
4
4
  *
5
5
  * The auto-compact gate counts tokens AFTER a turn lands. This module
@@ -165,6 +165,18 @@ const pugiSettingsSchema = z.object({
165
165
  // keeps Zod's strip-pass from swallowing it before the chain reader
166
166
  // sees it. See `hook-chains.ts` for the full schema.
167
167
  hooks: z.any().optional(),
168
+ // PUGI-260 — persistent default for the 1M context tier opt-in.
169
+ // `pugi config set context.tier 1m` writes this; per-invocation
170
+ // `--context-tier=...` flags override it. When omitted, the CLI
171
+ // sends no `contextTier` field на the wire (server treats as
172
+ // `standard` routing). The closed enum mirrors the CLI flag и the
173
+ // admin-api DTO; an unrecognised value triggers a Zod parse error
174
+ // at load time rather than a silent fallback.
175
+ context: z
176
+ .object({
177
+ tier: z.enum(['1m', 'standard']).optional(),
178
+ })
179
+ .optional(),
168
180
  });
169
181
  /**
170
182
  * #20 — the upstream tool drop-in compat ingest.
package/dist/index.js CHANGED
@@ -1,6 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { runCli } from './runtime/cli.js';
3
3
  import { PugiCliUpgradeRequiredError } from './core/transport/version-interceptor.js';
4
+ import { installSigintGuard } from './runtime/sigint-guard.js';
5
+ // PUGI-469 — install the top-level double-press Ctrl+C exit guard
6
+ // BEFORE any other init. Operators reported single ^C exiting the
7
+ // CLI; the guard requires a second press inside a 2s window (or
8
+ // emits a clean session-end envelope on the headless path) so a
9
+ // stray keystroke never kills the session. See the module header
10
+ // in `runtime/sigint-guard.ts` for the full behavior spec.
11
+ installSigintGuard();
4
12
  runCli(process.argv.slice(2)).catch((error) => {
5
13
  // PR-CLI-SERVER-VERSION-HANDSHAKE . When the admin-api returns
6
14
  // 426 Upgrade Required, the engine transport throws a typed
@@ -49,7 +49,7 @@ import { runUndoCommand } from './commands/undo.js';
49
49
  import { runCompactCommand } from './commands/compact.js';
50
50
  import { runRewindCommand } from './commands/rewind.js';
51
51
  import { runSessionsCommand } from './commands/sessions.js';
52
- // Day 4 ADR-0063: persona-memory operator surface (list / recall / write /
52
+ // Day 4 : persona-memory operator surface (list / recall / write /
53
53
  // forget / sync). The runner is shared by `pugi memory` top-level and the
54
54
  // in-REPL `/memory` slash so the two surfaces stay single-sourced.
55
55
  import { runMemoryCommand } from './commands/memory.js';
@@ -64,7 +64,7 @@ import { runRecipeCommand } from './commands/recipe.js';
64
64
  import { installDefaultSkills } from '../core/skills/defaults.js';
65
65
  // Backlog : bundled-skills batch 1 (stuck / simplify /
66
66
  // remember). Backlog : batch 2 (batch / verify / loop /
67
- // skillify, Nuekkis clean-room).
67
+ // skillify, external independent implementation).
68
68
  // Imported through the dedicated registry so future batches only append
69
69
  // to one barrel.
70
70
  import { runRememberCommand, runSimplifyCommand, runStuckCommand, runBatchCommand, runVerifyCommand, runLoopCommand, runSkillifyCommand, } from '../skills/bundled/index.js';
@@ -151,7 +151,7 @@ const handlers = {
151
151
  logout,
152
152
  lsp: dispatchLsp,
153
153
  mcp: dispatchMcp,
154
- // ADR-0063 Day 4: `pugi memory list|recall|write|forget|sync`. Routes
154
+ // Day 4: `pugi memory list|recall|write|forget|sync`. Routes
155
155
  // to `runMemoryCommand` (admin-api `/api/persona-memory` + offline
156
156
  // queue at `~/.pugi/memory-queue.jsonl`).
157
157
  memory: dispatchMemory,
@@ -233,7 +233,7 @@ const handlers = {
233
233
  // when no approval gate is wired. The existing `pugi memory write`
234
234
  // surface keeps its silent-enqueue behaviour for back-compat.
235
235
  remember: dispatchRemember,
236
- // Backlog — bundled-skills batch 2 (Nuekkis clean-room).
236
+ // Backlog — bundled-skills batch 2 (external independent implementation).
237
237
  // `pugi batch` fans out a YAML recipe of independent engine tasks
238
238
  // through up to --concurrency=N subprocesses (hard cap 30 per the
239
239
  // Mac safety carve-out). Aggregates results into
@@ -447,7 +447,7 @@ async function dispatchPrivacy(args, flags, _session) {
447
447
  });
448
448
  }
449
449
  /**
450
- * ADR-0063 Day 4 — `pugi memory <sub>` top-level dispatcher.
450
+ * Day 4 — `pugi memory <sub>` top-level dispatcher.
451
451
  *
452
452
  * Forwards to the shared `runMemoryCommand` runner. Exit codes:
453
453
  *
@@ -1597,7 +1597,7 @@ export async function runCli(argv) {
1597
1597
  // stderr line above the alt-screen flicker would race against the
1598
1598
  // banner paint on slow terminals.
1599
1599
  // Bare `pugi` on a TTY enters the REPL-by-default agentic session
1600
- // (Sprint , ADR-0056). The REPL is the customer-facing surface
1600
+ // (Sprint , ). The REPL is the customer-facing surface
1601
1601
  // that brings Pugi to parity with the upstream tool / Codex CLI. When the
1602
1602
  // operator has no credentials yet, we fall back to the splash
1603
1603
  // so the install-time `pugi` surface still shows the wordmark +
@@ -1824,7 +1824,7 @@ function parseArgs(argv) {
1824
1824
  }
1825
1825
  else if (arg === '--council') {
1826
1826
  // Backlog — opt-in council mode (Karpathy llm-council
1827
- // pattern, MIT clean-room TS port). Pairs with `--triple
1827
+ // pattern, MIT independent implementation TS port). Pairs with `--triple
1828
1828
  // --commit <SHA>`; the dispatch wraps the multi-provider
1829
1829
  // fan-out with an anonymous peer-review stage + chairman
1830
1830
  // synthesis. Costs ~2× tokens; explicit opt-in only.
@@ -4101,7 +4101,7 @@ async function performTripleProviderReview(root, session, flags, prompt) {
4101
4101
  // Server-side the controller also accepts `?council=true` query
4102
4102
  // string + `mode: 'council'` body shorthand; the CLI sends the
4103
4103
  // explicit boolean for forward compatibility. Inspired by
4104
- // Karpathy llm-council pattern (MIT). Clean-room TS port.
4104
+ // Karpathy llm-council pattern (MIT). independent implementation TS port.
4105
4105
  ...(flags.council ? { council: true } : {}),
4106
4106
  });
4107
4107
  writeFileSync(requestPath, `${JSON.stringify(requestBody, null, 2)}\n`, {
@@ -4961,6 +4961,38 @@ export function setHeadlessWriters(writers) {
4961
4961
  headlessStdoutWriter = writers.stdout ?? null;
4962
4962
  headlessStderrWriter = writers.stderr ?? null;
4963
4963
  }
4964
+ /**
4965
+ * PUGI-260 — read the persisted `contextTier` default из
4966
+ * `~/.pugi/config.json` (written by `pugi config set context.tier 1m`).
4967
+ * Returns `undefined` when the config file is missing, the key is
4968
+ * unset, or the value is invalid. Failures are silent — a malformed
4969
+ * persisted config must NEVER break the per-invocation dispatch path;
4970
+ * the flag default just falls back к "no preference" and the request
4971
+ * goes out on the standard lane.
4972
+ *
4973
+ * Kept inline (rather than importing from `commands/config.ts`) so the
4974
+ * dispatch path does not pull в the full `pugi config` command tree
4975
+ * during a `pugi code "..."` cold start — the import graph stays narrow.
4976
+ */
4977
+ function readPersistedContextTier() {
4978
+ try {
4979
+ const home = process.env.PUGI_HOME ?? resolve(homedir(), '.pugi');
4980
+ const path = resolve(home, 'config.json');
4981
+ if (!existsSync(path))
4982
+ return undefined;
4983
+ const raw = readFileSync(path, 'utf8');
4984
+ if (raw.trim() === '')
4985
+ return undefined;
4986
+ const parsed = JSON.parse(raw);
4987
+ if (parsed.contextTier === '1m' || parsed.contextTier === 'standard') {
4988
+ return parsed.contextTier;
4989
+ }
4990
+ return undefined;
4991
+ }
4992
+ catch {
4993
+ return undefined;
4994
+ }
4995
+ }
4964
4996
  function runEngineTask(kind) {
4965
4997
  return async (args, flags, session) => {
4966
4998
  const label = commandLabel(kind);
@@ -5250,18 +5282,29 @@ function runEngineTask(kind) {
5250
5282
  // point); `allowParallelAgents=false` strips the `agent` tool from
5251
5283
  // the schema so quick / standard tiers cannot accidentally fan out.
5252
5284
  intensityProfile,
5253
- // Task — 1M context tier opt-in. The operator passes
5285
+ // PUGI-260 — 1M context tier opt-in. The operator passes
5254
5286
  // `--context-tier=1m` to request the long-context lane. Server
5255
- // enforces Team-tier ($199) entitlement and rewrites the model к
5256
- // the `pugi-1m` alias. Lower tiers receive HTTP 402 with an
5257
- // upgrade-path message rendered by the dispatch error handler.
5258
- // `flags.contextTier` is intentionally NOT defaulted к 'standard'
5259
- // here `undefined` and `'standard'` are wire-equivalent (admin-
5260
- // api treats both as no-op), so the wire stays clean for callers
5261
- // that omit the flag entirely.
5262
- ...(flags.contextTier !== undefined
5263
- ? { contextTier: flags.contextTier }
5264
- : {}),
5287
+ // enforces Builder ($99) или Team ($199) entitlement и rewrites
5288
+ // the model к the `pugi-1m` alias. Lower tiers receive HTTP 402
5289
+ // with `reason: 'context_tier_requires_upgrade'` + the
5290
+ // `X-Pugi-Quota-Exceeded: context_tier_requires_upgrade` response
5291
+ // header rendered by the dispatch error handler.
5292
+ //
5293
+ // Resolution order:
5294
+ // 1. `--context-tier=1m|standard` flag (per-invocation override).
5295
+ // 2. `~/.pugi/config.json::contextTier` (persistent default,
5296
+ // set via `pugi config set context.tier 1m`).
5297
+ // 3. Omitted on the wire — admin-api treats omitted == standard.
5298
+ //
5299
+ // `'standard'` is wire-equivalent to omitted (the gate short-
5300
+ // circuits when `request.contextTier !== '1m'`), so the wire stays
5301
+ // clean for callers that omit the flag entirely.
5302
+ ...(() => {
5303
+ const effectiveTier = flags.contextTier ?? readPersistedContextTier();
5304
+ return effectiveTier !== undefined
5305
+ ? { contextTier: effectiveTier }
5306
+ : {};
5307
+ })(),
5265
5308
  });
5266
5309
  const toolCallId = recordToolCall(session, `engine:${adapter.name}`, `${label}: ${prompt}`);
5267
5310
  const taskId = `${kind}-${Date.now()}`;
@@ -7810,7 +7853,12 @@ export const __test__ = {
7810
7853
  // Backlog — `parseArgs` exposed under the test namespace so the
7811
7854
  // council-flag spec can assert flag-to-CliFlags mapping without
7812
7855
  // standing up a full execFileSync harness. Inspired by Karpathy
7813
- // llm-council pattern (MIT). Clean-room TS port.
7856
+ // llm-council pattern (MIT). independent implementation TS port.
7814
7857
  parseArgs,
7858
+ // PUGI-260 — exposed под the test namespace so the persisted-
7859
+ // contextTier-fallback spec can exercise the actual reader (с its
7860
+ // env-driven $PUGI_HOME redirect) without going через the full
7861
+ // engine-task dispatch path.
7862
+ readPersistedContextTier,
7815
7863
  };
7816
7864
  //# sourceMappingURL=cli.js.map
@@ -39,9 +39,41 @@ const configSchema = z
39
39
  privacy: z.enum(['local-only', 'metadata', 'full']).optional(),
40
40
  model: z.string().nullable().optional(),
41
41
  preferredEndpoint: z.string().url().optional(),
42
+ // PUGI-260 — persistent default for the 1M context tier opt-in.
43
+ // `pugi config set contextTier 1m` (or the dotted form
44
+ // `context.tier 1m`) writes this; per-invocation `--context-tier=...`
45
+ // flags override it at request time. Closed enum mirrors the CLI
46
+ // flag и the admin-api DTO so a typo here surfaces as a Zod parse
47
+ // error при load, not a silent fallback. Stored on the flat user-
48
+ // level config (~/.pugi/config.json) so all workspaces inherit the
49
+ // same default — operators с consistent long-context workloads
50
+ // (large monorepos, audits) set it once instead of remembering к
51
+ // pass --context-tier=1m on every dispatch.
52
+ contextTier: z.enum(['1m', 'standard']).optional(),
42
53
  })
43
54
  .strict();
44
- const CONFIG_KEYS = ['permissionMode', 'privacy', 'model', 'preferredEndpoint'];
55
+ const CONFIG_KEYS = [
56
+ 'permissionMode',
57
+ 'privacy',
58
+ 'model',
59
+ 'preferredEndpoint',
60
+ // PUGI-260 — exposed на `pugi config list` so operators see the
61
+ // current default. Hidden synonym `context.tier` accepted by
62
+ // runConfigSet / runConfigGet for a dotted-key familiar UX.
63
+ 'contextTier',
64
+ ];
65
+ /**
66
+ * PUGI-260: legacy / nested key aliasing. `pugi config set context.tier 1m`
67
+ * is the documented form в the feat doc; we normalise it onto the flat
68
+ * `contextTier` key before the strict-schema validation так future
69
+ * settings.json migrations keep one canonical key. Mirrors the
70
+ * legacy privacy-mode aliasing that already lives in the file.
71
+ */
72
+ function normaliseConfigKey(raw) {
73
+ if (raw === 'context.tier')
74
+ return 'contextTier';
75
+ return raw;
76
+ }
45
77
  export async function runConfigCommand(args, ctx) {
46
78
  const sub = args[0];
47
79
  if (!sub || sub === '--help' || sub === '-h') {
@@ -178,25 +210,27 @@ function isConfigKey(value) {
178
210
  return CONFIG_KEYS.includes(value);
179
211
  }
180
212
  function runConfigGet(args, ctx) {
181
- const key = args[0];
182
- if (!key)
213
+ const rawKey = args[0];
214
+ if (!rawKey)
183
215
  throw new Error('pugi config get requires a key.');
216
+ const key = normaliseConfigKey(rawKey);
184
217
  if (!isConfigKey(key)) {
185
- throw new Error(`Unknown config key "${key}". Allowed: ${CONFIG_KEYS.join(', ')}.`);
218
+ throw new Error(`Unknown config key "${rawKey}". Allowed: ${CONFIG_KEYS.join(', ')}.`);
186
219
  }
187
220
  const config = readConfig();
188
221
  const value = config[key] ?? null;
189
222
  ctx.writeOutput({ command: 'config.get', key, value }, value === null || value === undefined ? `${key} = (unset)` : `${key} = ${String(value)}`);
190
223
  }
191
224
  function runConfigSet(args, ctx) {
192
- const key = args[0];
225
+ const rawKey = args[0];
193
226
  const value = args.slice(1).join(' ');
194
- if (!key)
227
+ if (!rawKey)
195
228
  throw new Error('pugi config set requires a key.');
196
229
  if (value.length === 0)
197
230
  throw new Error('pugi config set requires a value.');
231
+ const key = normaliseConfigKey(rawKey);
198
232
  if (!isConfigKey(key)) {
199
- throw new Error(`Unknown config key "${key}". Allowed: ${CONFIG_KEYS.join(', ')}.`);
233
+ throw new Error(`Unknown config key "${rawKey}". Allowed: ${CONFIG_KEYS.join(', ')}.`);
200
234
  }
201
235
  const current = readConfig();
202
236
  // Build the candidate and validate via the schema so an invalid value