Skip to content

KeylessAgent: full agent stack — parser hardening, browser reliability, WPF UI, snapshot budgets, NuGet publishing - #2

Merged
ImanEstiri merged 152 commits into
mainfrom
feature/keyless-agent
Nov 5, 2026
Merged

ImanEstiri merged 152 commits into
mainfrom
feature/keyless-agent

Conversation

@ImanEstiri

Copy link
Copy Markdown
Owner

Merges the complete feature branch into main (main PR #1 was an early squash of this same branch).

Highlights since PR #1

  • Parser & reply pipeline: corruption auto-repair, outer-fence stripping, lenient one-line shapes, wall-clock truncation budget, copy-button circuit breaker, selectors.json versioning with refresh
  • Browser reliability: explicit Playwright timeouts everywhere (no 30s defaults), composer suspect safety net, tiny-corner window geometry (work-area aware, PerMonitorV2 DPI), show/hide/close window control with honest verification, chrome diagnostics command
  • WPF UI: full Midnight & Champagne desktop app (agent, doctor, settings, workspace, undo pages) with Start/Show/Hide/Close Chrome
  • Config: schema migration v1→v2, known-stale-default upgrades, TreeAndChanged snapshot mode with total-size budget, per-file caps and generated-file denylist
  • Publishing: NuGet tool packaging (icon, README, PackAsTool keyless), publish.ps1/publish.bat flow with Directory.Build.props version bumping

Merged tree verified: build clean, 641 core + 211 WPF tests green, dotnet pack smoke-tested.

… tooling

- KeylessAgent.slnx with Abstractions, Core, Cli, Playground, and Tests projects
- Directory.Build.props (net10.0, analyzers, package metadata) and Directory.Packages.props (CPM)
- All NuGet packages installed via dotnet CLI (ImanSoftware.* set, Playwright, Spectre, xunit, FluentAssertions 7.x)
- nuget.config with package source mapping, publish.ps1, build-and-test workflow
- .editorconfig, .gitattributes, .gitignore, global.json, LICENSE.txt, tool icon
- CommandKind with Description attribute per AgentScript v1 command name
- CommandStatus, ApprovalMode, SnapshotMode, and AgentEventType enums
- Line-based state-machine parser: all 10 commands, case-insensitive keywords,
  multiline payloads, nonce markers (<<<CONTENT:x7), code-fence stripping,
  line-numbered problems for every grammar violation
- AgentScriptValidator: response/file-content size limits and path shape rules
- ResponseFormatGuard: single gate producing FormatGuardResult (valid script or problems)
- ErrorCodes catalog (KA_*), configuration options model, format-correction.txt template
- 28 xunit tests covering all parser and guard paths
…ckups

- GitService via short-lived git processes (info, branch, stash, stage,
  commit, push -u, log, .git/info/exclude upkeep); never force or reset
- BranchGuard enforcing protected branch patterns (main/master/release*) in code
- BranchNameBuilder: agent/<yyyyMMdd-HHmm>-<slug> with request-derived kebab slug
- PathSandbox: workspace-relative only, no .. or absolute paths, .git/.agent
  blocked, symlink escape detection
- CommandPolicy: deny list (format/shutdown/force-push/global-config/pipe-to-shell)
  plus configurable allow list for AutoSafe
- Per-command executors (say/newfile/updatefile/editfile/deletefile/readfile/
  runcommand/ask/commit/done) behind WorkspaceCommandExecutor with dry-run,
  Ask-mode diffs/confirmations, BOM preservation, and backup-before-change
- BackupService into .agent/backups with undo-latest restore
- Tests for path sandbox (.., absolute, .git/.agent, junction escape),
  command policy (deny list, segments, allowlist), branch guard
  (main/MASTER/release*/releases vs working branches), branch name builder
  (fake clock, empty/non-Latin/symbol/long slugs), and all executors
  (new/update/edit/delete/read/run/ask/commit incl. BOM preservation,
  backups, dry-run, declines, push failure, non-zero exits)
- Fix sandbox: symlink check no longer rejects paths whose parent
  folders do not exist yet
- Slug: whitespace collapses to dashes, repeated dashes collapse
- Relax analyzers for the test project (not shipped)
- SnapshotService runs tree-view.bat/code-dump.bat via cmd /c without a
  window, stdin closed and hard timeout so the trailing pause can never hang;
  staleness check ignores the snapshot outputs themselves
- SnapshotFilter: tree lines filtered by folder name including nested blocks;
  dump sections dropped by heading; chunking only at ### section boundaries
- ResultFormatter builds [KEYLESSAGENT RESULTS] messages mapping every result
  to command number/name/target with head+tail output capping and the
  skipped-commands note
- Tests: filter/chunk, bat-run-without-hanging, staleness, attachments,
  formatter ids/truncation/skipped
…extractor, fake

- PlaywrightChatProvider: ConnectOverCDP to the user's Chrome (default) with
  LaunchPersistentContext fallback, manual-login wait, new-chat click, and
  DeepThink/Search toggle enforcement via aria-pressed (warn and continue
  when a toggle cannot be found)
- PlaywrightChatSession: rate-limited sends, whole-text insert (never
  per-character typing), attachment upload via file input with paste
  fallback, reply waiting on stop-indicator + stable final-answer read,
  debug screenshots
- Default selectors.json built from the real DOM sample (structural
  selectors over hashed classes) with fallback lists, editable without
  recompiling, auto-written to the workspace
- ReplyExtractor: tag-scanner returning only the final answer, skipping
  .ds-think-content and code-block banners, preserving pre indentation
- FakeChatProvider (scripted replies) + local RateLimiter
- Playground harness running one scripted conversation end to end
- 139 tests green; solution builds with zero warnings
- AgentLoop orchestrating one turn in numbered steps: config load, git
  preconditions (repo check, dirty-tree continue/stash/cancel), protected-
  branch guard with agent-branch creation, session resume/new chat,
  stale-snapshot refresh, first message with system prompt + attachments,
  the conversation loop (wait -> guard -> execute with batch limits and
  skipped reporting -> results message), and an always-on snapshot refresh
  in finally
- Automatic format re-ask with correction template up to MaxFormatRetries,
  raw reply shown to the user when exhausted
- ConfigurationService: tolerant keyless.json (comments, case, trailing
  commas, string enums), default file written when missing, range validation
- SessionStore persisting chat URL/branch/turn in .agent/session.json
  (no-session is a KA_SESSION_NOT_FOUND failure, matching the Outcome
  package's rule that Data is never null)
- SystemPromptBuilder injecting limits into the embedded system-prompt.md
- KeylessAgentServiceFactory wiring everything by hand (no DI container)
- TurnLogger JSON-lines transcript, FileLoggerProvider for debug diagnostics
- 156 tests green including end-to-end loop tests with the fake provider
…ckaging

- SpectreAgentUserInterface: say messages as panels, progress through the
  status spinner, prompts for confirm/ask, colored +/- diffs before changes
- Commands: 'run <request>' (--dry-run, -v), interactive default command
  (prompt-per-turn session), 'resume <request>' (reuses .agent/session.json
  chat), 'undo' (restores newest backup batch), 'config' (prints effective
  keyless.json, writing defaults when missing)
- AgentCliRunner: shared spinner + branch/commit summary at turn end
- KeylessAgentServiceFactory.CreateSessionStore/CreateFileLoggerFactory;
  SingleProviderLoggerFactory for verbose diagnostics logging
- publish.bat calling the root publish.ps1; tool verified to pack (nupkg+snupkg)
- Full README (what/why, requirements, install, usage, Mermaid agent-loop
  flowchart, AgentScript summary, safety model, known limitations, roadmap
  incl. WinForms UI and official-API provider, ToS/account-risk notice,
  not-affiliated statement) and the short tool README
- docs/: architecture.md, agentscript-spec.md (grammar, commands, limits,
  full KA_ error-code catalog), configuration.md, browser-automation.md
  (selectors table + manual verification checklist), security.md,
  git-workflow.md
- Checklist review: inject IDateTimeProvider into the browser classes so
  Core has no direct DateTime.Now/UtcNow; verified zero warnings, zero
  csproj package versions, exactly six ImanSoftware packages, no
  Spectre/console usage in Core — 156 tests green
…nt directory

- All commands now take the workspace path as their first positional
  argument: 'keyless run <workspace> <request>', 'resume <workspace>
  <request>', 'undo <workspace>', 'config <workspace>'
- When omitted, the very first prompt asks for it, validating that the
  directory exists (current directory shown only as the default, never
  silently assumed); run/resume also prompt for a missing request
- Interactive default command prompts for the workspace before the first
  request; -w removed in favor of the positional argument
- README (root + tool) usage examples updated to workspace-first form
…-fatal

Bug: wrapping a whole agent turn in AnsiConsole.Status made every Say/
Confirm/Ask during the turn throw Spectre's 'Trying to run one or more
interactive functions concurrently' and kill the process with exit 1.

- IAgentUserInterface gains BeginWait(message): Core now marks only actual
  waiting phases (chat reply, sends, runcommand process, snapshot scripts)
  with a disposable wait scope — never the whole turn
- New ConsoleUserInterface owns all console access through one semaphore;
  the spinner is a background task cancelled+joined before ANY other UI
  call (Say/Ask/Confirm/ShowDiff/ShowProgress), overlapping BeginWait is
  safe (previous spinner stopped first), scopes tolerate double stops
- AgentCliRunner no longer wraps turns in Status; a failing or cancelled
  turn is printed (message + .agent/logs pointer) instead of crashing
- InteractiveSession loop: turn failures return to the keyless> prompt,
  Ctrl+C cancels only the current turn (fresh linked token per turn);
  exit code 1 reserved for startup errors
- Console Output/InputEncoding set to UTF-8 at startup when not
  redirected; console markers switched to plain ASCII ([ok] [!] [x] [i])
  with proper markup escaping
- Tests: nested-exclusivity reproduction of the crash, BeginWait+Confirm
  stops spinner first, Say during a wait, overlapping waits, Ask after a
  disposed wait, and failing/cancelled turns returning to the prompt
  (Spectre.Console.Testing 0.57.2 via CPM); 164 tests green
Before every send the newest assistant message is fingerprinted
(data-virtual-list-item-key, else a hash of the final-answer text, plus
the visible count); the send itself must be confirmed within
Browser.SendConfirmSeconds (new, default 15: new message item, or empty
input + generating indicator) with one retry (click then Enter) before
failing KA_SEND_NOT_CONFIRMED; WaitForReplyAsync accepts only a reply
whose fingerprint differs from the pre-send one (identical text valid
only with a new message key), whose final answer exists and is stable
for two reads, and once the stop indicator is gone. Attachment sends
wait for chips + enabled send (Browser.UploadWaitSeconds, default 60)
and fall back to pasting on timeout.

Session logic now sits behind the IChatPage seam (PlaywrightChatPage =
real DOM; FakeChatPage in tests), enabling scenario tests (a)-(d).
Docs updated; 168 tests green.
- LineEndings helper: detect dominant ending (mixed keeps the majority,
  tie means LF), normalize to LF, apply back; new files resolve their
  ending from .editorconfig (end_of_line, section globs), then
  .gitattributes (eol=), then the dominant ending of sibling source files
  (dot/config files excluded), then Environment.NewLine
- editfile: match and count FIND/REPLACE on LF-normalized text (the
  'matched N times' message included) and write back with the file's own
  ending; updatefile converts model content to the existing ending;
  newfile applies the resolved ending; BOM handling unchanged
- FileChange diffs are LF-normalized (no stray \r in the CLI diff) and
  MaxCommandOutputChars counts normalized characters in result messages
- Tests: CRLF+LF FIND matches once and stays CRLF, multi-line find,
  mixed-ending file, BOM+CRLF, newfile obeys .editorconfig and Windows
  NewLine, updatefile keeps endings, editorconfig globs, gitattributes,
  sibling dominance, diff without \r; 185 tests green
- deletefile now asks for confirmation in Ask AND AutoSafe modes; only
  Auto skips it (a backup is always made in every mode)
- AutoSafe/Auto turns show a one-time warning at turn start:
  'AutoSafe is not a security boundary' (dotnet/node/python can run
  arbitrary code)
- docs/configuration.md, docs/security.md and the README approval table
  now state exactly what the code does
- Tests: deletefile asked+declined in Ask and AutoSafe, not asked in
  Auto, and the AutoSafe boundary warning; 186 tests green
- BeginWait returns only after the Status display actually started
  (ManualResetEventSlim set from inside the display), so a prompt can
  never start in the gap; StopSpinner no longer gives up after 3s — it
  joins fully (30s upper bound with a warning), so any prompt/write
  always runs after the spinner is really gone
- ConsoleWriter becomes an instance over one IAnsiConsole (Shared for
  command output); commands and the runner route everything through the
  same console surface as the agent UI
- Turn failures print exception type + message; with -v the full stack
  trace is shown and appended to .agent/logs/diagnostics.log; the log
  path is always printed; WorkspacePrompt uses the same ASCII [x] marker
- Tests: prompt immediately after BeginWait never throws, slow spinner
  shutdown still lets the following prompt run, failing turns show the
  exception type and return to the keyless> prompt; 190 tests green
- SendButton fallback order per the sample: icon button in the textarea's
  sibling container, iconLabelPrimary, filled-circle, filled next to the
  file input, submit — each candidate checked for visibility and
  aria-disabled before clicking (a disabled send is skipped, never the
  target); Enter stays the last fallback
- Every selector loop logs (Debug) which fallback INDEX matched, so a
  broken selector is one log line away
- Toggle and MessageItems selectors made pure-CSS first (:has-text kept
  as a Playwright-only later fallback); AttachmentChips added earlier
- Fragment tests with AngleSharp run the default catalog against real
  fragments of deepseek-dom-sample-trimmed.html: input placeholder, file
  input multiple, toggles aria-pressed, virtualized item keys, final
  answer vs reasoning, send fallbacks matching and skipping the disabled
  button; 198 tests green
- /workspace <path> validates the directory, refuses while the loop is
  not ready (a failing switch keeps the old workspace), re-runs the git
  preconditions and clears the target workspace's .agent/session.json
  (fresh chat session), then prints the new header; every turn and
  callback receives the workspace explicitly — no CurrentDirectory
- /help lists the slash commands; any other /-prefixed input is rejected
  with a hint instead of being sent to the model
- InteractiveSession takes the workspace as a parameter and a
  switchWorkspace callback; InteractiveCommand wires preconditions via
  the git service and session store
- Tests: help listed without a model send, unknown /command rejected,
  switch recorded + used for subsequent turns, missing directory
  refused, failed switch keeps the old workspace, usage message;
  204 tests green
- Send retry happens only when the input STILL holds the text; a cleared
  input means the message was sent even when the virtualized item count
  did not change (treated as sent, then WaitForReplyAsync). Retry exactly
  once, and each attempt is the send button when clickable, otherwise
  Enter — never both in the same attempt (first attempt included)
- Attachment delivery judged by several signals: SetInputFiles result,
  the hidden file input's reported file names, composer-near elements
  showing a file name, chips visibility. Paste only when SetInputFiles
  itself failed or there is NO sign of the files after
  Browser.UploadWaitSeconds; uncertainty counts as attached with a
  warning — the same content is never both attached and pasted
- FakeChatPage gains FailClick, InputFileNames, VisibleFileNames and
  LastInsertedText; tests (a)-(e) cover all five scenarios; 209 tests green
- IChatDiagnostics (Core, GUI-reusable) + ChatDiagnosticsService over the
  IChatPageInspector seam: probes every selectors.json key through the
  fallback lists and classifies MATCHED (index + count) / NOT FOUND /
  NOT APPLICABLE NOW (Stop idle, no chips, no reasoning, fresh chat,
  login); reports login state, DeepThink/Search aria-pressed, input
  editability and the page URL; exit 0 only when all required keys
  matched; never sends a message
- PlaywrightChatInspector (real DOM) with the shared connection logic
  from PlaywrightChatProvider; a new tab, never touching the user's chat
- --capture <dir>: trimmed HTML snapshots (composer, newest message,
  sidebar, chips) via HtmlTrimmer — scripts/styles/svg removed, long
  text replaced with '...' so no personal content leaves the machine
- --watch: waits for a manually sent prompt, captures generating and
  finished states once each
- keyless doctor CLI command with a state table; docs section 'How to
  verify selectors: keyless doctor'; 5 tests with a scripted inspector;
  214 tests green
- All IChatPage operations are Task-returning with a CancellationToken;
  Playwright's waits have no token support, so each call races the token
  (WithCancellationAsync) — Ctrl+C interrupts any in-flight wait promptly
  instead of waiting out its timeout; FakeChatPage and all session tests
  updated
- KA_SEND_NOT_CONFIRMED and KA_REPLY_TIMEOUT errors now carry a pointer:
  the debug screenshot path under .agent/logs when Browser.Debug is on
  (the page writes to the exact path), otherwise a hint to run
  'keyless doctor'; screenshot path bug fixed (the destination path, not
  the name, is passed to the page)
- docs/browser-automation.md: send/confirm and attachment decision
  tables, the still-unverified selector list (Stop, New Chat, chips),
  and the cancellation note
- 216 tests green; build with zero warnings
…ibutes

- HtmlTrimmer rewritten: removal tags became HTML comments (the old
  '<script removed>' literal parsed as a real element); attribute values
  (href/src/title/alt/value/placeholder/aria-label/data-*) are masked to
  '[masked]' unless they are short allowlisted UI labels; structural
  attributes (class/role/aria-pressed/aria-disabled/type/accept/multiple)
  survive; data-virtual-list-item-key is replaced with a stable '[key:hash]'
- Text runs longer than the cap no longer swallow the following '<'
  (old '>...<' bug) — text nodes are masked per-node instead of one greedy run
- Allowlist: DeepThink, Search, New chat, Stop, Message DeepSeek,
  'Thought for N seconds' pattern, Copy, Download, Regenerate
- Tests with AngleSharp: sidebar titles/hrefs, user messages and
  attachment file names never appear; UI labels + structural attributes
  survive; trimmed HTML parses to the same element structure (minus the
  removed script/style/svg); 223 tests green
… readiness

- IsSendEnabledAsync is tri-state now: true/false when a send control
  matched, null when no send-control selector matches at all (fake page
  and real page agree); a null state never blocks the send
- After SetInputFiles succeeds, the session waits (up to
  Browser.UploadWaitSeconds) for BOTH a positive upload sign (files
  registered in the input, a chip showing a file name, chips visible)
  AND an enabled send control, whenever that control can be detected;
  an upload registered while the control stays disabled is sent anyway
  with a warning after the timeout
- Tests: send enabled only after N polls -> the click happens after
  enablement; undetectable send control -> no deadlock (returns well
  before the timeout); 225 tests green
- SelectorsCatalog.LoadAsync, PlaywrightChatInspector (ProbeAsync/
  GetToggleStateAsync/IsInputEditableAsync/CaptureTrimmedAsync) and the
  whole diagnostics pipeline are async with CancellationToken
- FileLoggerProvider no longer blocks inside ILogger.Log: Log enqueues
  into a Channel<string> and one background task performs all storage
  I/O; the provider constructor does no I/O at all
- TurnLogger.StartAsync/AppendAsync/AppendResultAsync; AgentLoop awaits
  them; /workspace switch flow is async end to end (git preconditions and
  session clearing happen on the async path with the turn's token)
- New NoSyncOverAsyncRuleTests greps all src/ production sources for
  GetAwaiter().GetResult()/.Result/.Wait() and fails on any violation
  (tests are exempt); 225 tests green, zero warnings
…tab rules

Observed bug: a separate logged-out Chrome window opened (never focused)
showing the DeepSeek login page instead of the user's session.

Root causes fixed:
- Chrome 136 ignores --remote-debugging-port for the DEFAULT
  user-data-dir: the docs claiming a daily-profile attach were wrong and
  are corrected everywhere (README requirements, browser-automation
  Connection section, configuration.md). A dedicated profile
  (Browser.UserDataDir, default %LOCALAPPDATA%\KeylessAgent\chrome-profile)
  is now mandatory and the login persists in it
- The silent LaunchPersistentContext fallback is removed; it is opt-in
  (Browser.AllowPlaywrightLaunch=false) with an explicit warning
- pages[last] navigation (which could destroy a user's tab) is gone:
  an existing chat.deepseek.com tab is reused (Browser.ReuseExistingTab),
  otherwise a new TAB opens via CDP Target.createTarget newWindow:false,
  matched by URL polling; the tab is focused (BringToFrontAsync +
  Target.activateTarget), again when login is required
- Connection is now IBrowserConnector in Core (probe -> attach;
  auto-launch the REAL Chrome via IBrowserEnvironment: port probe,
  chrome.exe locate via config/registry/paths, Process.Start, wait 30s,
  attach; actionable KA_BROWSER_CONNECTION_FAILED with the exact manual
  command line; port-open-but-attach-failed reports the reason instead
  of falling through), shared by run, doctor and the new
  'keyless chrome <workspace>' command (dedicated Chrome + one-time login)
- Dispose policy: only the tab the agent opened is closed; the Chrome
  window stays running (Browser.CloseChromeOnExit=false default)
- New config keys: UserDataDir, AutoLaunchChrome, ChromePath,
  ReuseExistingTab, AllowPlaywrightLaunch, CloseChromeOnExit
- Tests with faked environment: port open -> attach (no relaunch),
  port closed -> launch then attach, Chrome missing -> actionable
  failure, auto-launch off -> fail instead of fall through, Playwright
  fallback opt-in surfaces as failure without a real browser, profile
  default resolution; 232 tests green
- IAgentUserInterface.ChooseAsync with ChoiceRequest/ChoiceOption/ChoiceResult
  (UI-agnostic); ConsoleUserInterface numbered menu: arrows move, digits 1-9
  pick immediately, Enter confirms the highlighted (default marked), Esc/Ctrl+C
  cancels; non-interactive input returns the default and prints it; runs inside
  the console gate (spinner stopped first)
- AgentLoop preconditions rewritten as one combined flow: at most ONE menu per
  turn, none when the tree is clean and the branch is protected (agent branch
  auto-created); dirty menu carry/stash/here/cancel (here omitted on protected
  branches); branch menu new/stay/cancel on clean non-protected branches; the
  typed 'type continue/stash/cancel' and 'press Enter or type keep' prompts removed
- Auto git init (Git.AutoInit default true): git missing -> KA_GIT_NOT_INSTALLED
  with download link + manual steps; git init -b main (fallback init + branch -M
  main); .NET .gitignore via dotnet new gitignore or built-in template when the
  workspace has .sln/.slnx/.csproj; initial commit 'Initial commit (created by
  KeylessAgent)' with one-shot -c user.name/user.email identity fallback
- CommitExecutor: no 'origin' remote -> commit locally and skip push with a
  notice in the result sent to the model; origin present + push failure reported
  as before; KA_GIT_INIT_FAILED for init/commit failures; AgentTurnResult.ErrorCode
- ConfirmAsync shows the bracketed default, Enter accepts, no empty-input loop
- 233 tests green
…back

- Browser.ReplySource = Auto | Copy | Dom (default Auto); Auto tries the
  Copy capture, validates, falls back to Dom; Copy never falls back
  silently (KA_REPLY_TIMEOUT); Dom is the original behavior
- New selector key MessageCopyButton (message-level Copy copies ONLY the
  final answer; bar may be hover-only — the newest message is hovered
  first, and only the NEWEST message's control is ever clicked)
- IChatPage capture members + Playwright implementation: ExposeBinding
  interception wrapping navigator.clipboard.writeText, execCommand('copy')
  and the copy event, gated on an agent-only flag (user's manual copying
  and the OS clipboard are never affected); clipboard fallback grants
  clipboard-read/write, saves the previous text, reads readText() and
  ALWAYS restores it in a finally block
- ReplyReader: strategy + validation (normalized markdown/punctuation-
  stripped comparison: containment or similarity >= 0.9; reasoning start
  must be absent; copy >= 50% of DOM length; numbers logged on rejection)
- Completion detection unchanged (fingerprint + final answer + stop +
  stable); IChatSession.LastReplyOrigin surfaces Copy/Dom/Dom (copy
  rejected) and the AgentLoop logs reply-source per reply
- Parser fence tolerance: outer fence stripped only when the first AND
  last non-blank lines are fences of the same or greater length (3 or 4
  backticks); inner fences inside CONTENT payloads untouched; system
  prompt tells DeepSeek not to wrap replies in a code fence
- doctor: MessageCopyButton probe + ReplyCapture check (strategy,
  clipboardUntouched, copiedLength, validation result)
- 247 tests green (14 new: ReplyReader validation/strategies, parser
  fences, interception/clipboard/none flows)
…ODO removed

- README rewritten: tagline, requirements (.NET 10, Windows, git, Chrome with
  the Chrome 136 dedicated-profile rule), install, first run, usage with the
  numbered git menus and auto-init notice, Mermaid agent loop, AgentScript
  summary, reply sources table (Copy/Dom), safety model ('AutoSafe is not a
  security boundary'), known limitations, roadmap, layout, ToS/not-affiliated
- docs: git-workflow.md (auto init, numbered menus, at-most-one-menu flow,
  no-origin push skip), security.md (menu note), configuration.md
  (ReplySource + Git.AutoInit), browser-automation.md (reply sources,
  capture strategies, validation rules), agentscript-spec.md
  (KA_GIT_NOT_INSTALLED, KA_GIT_INIT_FAILED)
- CLI README updated (chrome/doctor commands, menus, auto init)
- DocsConsistencyTests: every ErrorCodes KA_ code must appear in
  agentscript-spec.md; every Browser/Limits/Git/Commands default must appear
  in configuration.md; the documented keyless.json block must deserialize to
  values equal to the default options object
- DOCS-TODO.md deleted; 250 tests green
…ules, verified result lines, step-mode iteration raise
…ols wired into the executor chain, e2e hardening scenarios
…tips, TextMuted contrast, inverse-visibility converter
… diagnostic

Priority 1 — schema version and migration: KeylessOptions gains
SchemaVersion (null = pre-versioning v1), CurrentSchemaVersion = 2 and
CreateDefault(); LoadAsync migrates stale files (backs up the raw file as
keyless.json.v<N>.bak, preserves Mode/ReplyTimeoutSeconds/ChromeDebugPort/
Allowlist, replaces the rest with current defaults) and AgentLoop announces
the migration. A fresh keyless.json is now written at the current version so
it is never migrated pointlessly. Fixed stale defaults that overrode the new
C# timings (MinDelayMs 400, MaxDelayMs 1500, Commands.TimeoutSeconds 180);
docs/configuration.md documents the new values plus SchemaVersion.

Priority 2 — per-message reminder no longer contradicts the CONTENT fence
rule: the whole reply is plain AgentScript, but every CONTENT payload must
be wrapped in its own triple-backtick fence (stripped by the program), with
a worked example; system-prompt.md says the same right after the CORRECT
example.

Priority 3 — every Playwright call carries an explicit timeout (clicks
5s/3s, fills 5s, hovers 2s, reload 10s, combined waits 5s, goto commit 15s,
CDP minimize race 5s, session insert 15s / attempt-1 click 10s via linked
CTS) so nothing rides on the 30s library default.

Priority 4 — copy-button diagnostic: PlaywrightChatInspector.DumpButtonsAsync
dumps tag/role/label/title/text/classes of every button on the newest
assistant message; keyless doctor appends the first 400 chars to the
MessageCopyButton detail; a third label-scan strategy clicks any button
whose label contains 'copy'.
…istence

A1 — the 30 s Playwright default in selector probes (the 60/120/180 s
send/results/upload multiples): EvaluateAsync is hard-capped at 1.5 s;
IsSendEnabledAsync skips absent selectors with an instant CountAsync (no
element resolution at all) while keeping the paperclip skip and the
null=undetectable contract; SetInputFiles gets 5 s, the accessibility
hover 2 s; page.SetDefaultTimeout(5000) lands on every connection path
(attach, auto-launch, Playwright fallback, doctor) as the safety net;
IsSendEnabledAsync and ResetComposerAsync log a warning above 2 s with
the slowest selector index.

A2 — the ~10 s copy wait per message: first wait capped at 3 s, and a
per-session circuit breaker skips the whole copy path after the first
miss (origin 'copy disabled after the first miss this session'); a new
session or keyless doctor --capture re-opens it; Browser.ReplySource=Dom
still short-circuits with zero waits.

B — the Chrome window popping back up: Target.createTarget now sends
background=true (Chrome activated the new tab and restored the minimized
window); the minimize runs immediately after the tab opens (Minimized +
MinimizeAfterLogin) and again after login; CDP verifies the state via
Browser.getWindowBounds read-back with a normal->minimized recovery (up
to 3 attempts); the Win32 fallback is real now — ChromeWindowMinimizer
enumerates Chrome_WidgetWin_1 windows owned by our chrome.exe processes
(launched PID's descendant tree, or the --user-data-dir / debug-port
command-line match when attaching) and ShowWindow(SW_MINIMIZE)s them,
because the launcher PID's MainWindowHandle is typically 0. BringToFront
stays login-only.

Tests: absent send-button selectors do zero EvaluateAsync calls; the copy
circuit breaker (one wait per session, 3 s cap) and the Dom short-circuit;
createTarget background/newWindow parameters. DOCS-TODO entries mark the
minimize and copy behavior UNVERIFIED until a live run.
…guard

Turn-9/10 evidence: a minimized Chrome window gets throttled by Windows 11
native occlusion detection, the DeepSeek stream stops mid-reply, the Stop
indicator blinks off between chunks, and WaitForReplyAsync accepted the
truncated AgentScript reply.

CHANGE 1 — ComposeChromeArguments gains
--disable-features=CalculateNativeWinOcclusion,IntensiveWakeUpThrottling,
--disable-ipc-flooding-protection and --disable-hang-monitor (existing
flags untouched). The window still minimizes visually; the renderer keeps
running at full speed.

CHANGE 2 — WaitForReplyAsync never accepts an AgentScript reply whose tail
lacks @@eof: it logs, keeps polling (the stream may resume), and after 5
truncation detections fires a ONE-SHOT FocusTabAsync recovery (counter
reset, never routine); 5 more detections even with the window visible fail
with KA_REPLY_TRUNCATED ('cut off repeatedly even with the window visible.
The chat session stays valid. Resend the same request.'). MinimizeAfterLogin
and WindowMode defaults are untouched; no un-minimize outside this recovery.

CHANGE 3 — every detection logs 'Truncated reply detected (attempt N):
length=.. last60=..' and appends a 'reply-truncated' turn event with
Length/Tail/Attempt/FocusTriggered.

Tests: truncated-then-completed accepted only after @@eof arrives; 5 → one
focus recovery → 5 → failure with 10 events and the FocusTriggered flags;
the occlusion flags theory. 601 core + 201 WPF tests green.
- WaitForReplyAsync no longer focuses the tab on truncation by default:
  20 silent detections (reset stableReply, Warning log, reply-truncated
  event with FocusTriggered:false) then KA_REPLY_TRUNCATED with the resend
  hint ('Reply stream was cut off 20 times in this turn. The chat session
  stays valid. Resend the same request. If this repeats, set
  Browser.AutoFocusOnTruncation to true in keyless.json, or use
  WindowMode:Offscreen.'). No FocusTabAsync call site remains in
  SendMessageAsync (verified: there never was one in this session file).
- BrowserOptions.AutoFocusOnTruncation (default false): when true, ONE
  FocusTabAsync per WaitForReplyAsync at the fifth detection — the only
  focus call site in the session. docs/configuration.md documents it;
  MinimizeAfterLogin, WindowMode and the Chrome flags untouched.
- CLI confirmation prompt rings the terminal bell ('\a') so the taskbar
  flashes without raising a window.
- Tests rewritten: 20-detection default path never focuses and carries the
  exact failure message; opt-in path focuses once at the 5th detection
  (deterministic swap in the 5th event's callback) and still accepts the
  completed reply. 602 core + 201 WPF tests green.
New IChromeWindowController (ChromeWindowController.cs): ConnectOverCDP 10s,
first context/page (creates one when empty), Browser.getWindowForTarget ->
windowId, Show = setWindowBounds normal @100,100 1280x900 + Target.activateTarget,
Hide = setWindowBounds normal @-32000,-32000 (a moved window keeps rendering;
a minimized one is throttled), --peek N shows then restores offscreen after N s,
closed port fails with 'Chrome is not running on port {port}. Run keyless chrome
first.', session detached in finally, every CDP call logged. Factory:
CreateChromeWindowController(loggerFactory).

CLI: keyless show [workspace] [--peek N] and keyless hide [workspace], following
the DoctorCommand pattern; exact success strings per spec. 'keyless chrome' shows
the window right after ConnectAsync so an offscreen login page is visible. The
login flow (WaitForLoginIfNeeded) shows the window instead of BringToFront —
FocusTabAsync stays only as the unused IChatPage contract there.

WPF: ChromeViewModel gains IChromeWindowController + ShowChromeCommand (disabled
without a workspace), DoctorView adds a 'Show Chrome' GhostButton next to Start
Chrome, ShellWindow wires the factory. Test ctors updated.

Tests: ChromeWindowControllerTests (closed-port message, exact show/hide CDP
payloads, empty-context page creation, CDP failure -> Outcome), provider login
tests assert ShowAsync(peek 0) instead of FocusedTabs, WPF ShowChrome tests.
607 core + 204 WPF tests green. WindowMode/MinimizeAfterLogin/flags untouched.
…ide Chrome button

Turn-16 proved Chrome was TRULY minimized (throttled renderer, 20 reply
truncations), not hidden:
- WindowMode default 'Minimized' → 'Offscreen' ('Minimized' kept as a
  backward-compatible alias that also means offscreen: launch flags
  --window-position=-32000,-32000 --window-size=1280,900; --start-minimized
  is gone — a minimized window is throttled by Chrome no matter what the
  startup position was).
- PlaywrightChatProvider.MinimizeWindowAsync → HideWindowAsync: the CDP
  call now sends {left:-32000, top:-32000, width:1280, height:900,
  windowState:normal} and VERIFIES via Browser.getWindowBounds read-back
  (left/top == -32000) with a normal→offscreen recovery (3 attempts); the
  Win32 fallback ShowWindow(SW_MINIMIZE) → SW_HIDE. Early-hide and
  post-login hide both cover every non-Normal mode. Recovery: keyless show
  / WPF Show Chrome.
- Doctor page: Start Chrome / Show Chrome / Hide Chrome (GhostButton,
  tooltips, cross-disable while busy). ChromeViewModel gains
  HideChromeCommand + IsHiding; all three commands cross-notify.
- keyless config now prints windowMode/minimizeAfterLogin and escapes the
  bracket-joined list cells (the '[main, master, release*]' cell parsed as
  Spectre markup and crashed the command).
- Tests: WindowMode theory rewritten (no --start-minimized anywhere except
  Normal's absence), provider login tests unchanged, WPF hide/cross-disable
  tests, config-table fix. 607 core + 208 WPF tests green; WPF Release
  build clean; fresh keyless.json contains WindowMode Offscreen.
…me diagnostics

Turn-17 (WindowMode=Offscreen, -32000,-32000) truncated 20 times WITH
--disable-features=CalculateNativeWinOcclusion present; turn-18 succeeded
only while the window was visible and truncated again when hidden.
Conclusion: Chrome treats fully offscreen windows as occluded and
throttles the renderer regardless of the startup position.

STEP 1: launch command line is logged by BrowserEnvironment.StartChromeAsync
and forwarded by the connector as a 'chrome-launched' event into the turn
log (provider wires ChatSessionOptions.LogEvent into the new internal
BrowserConnector.LogEventSink).

STEP 2: WindowModeFlag gains TinyCorner (--window-position=w-80,h-80
--window-size=80,80) and EdgePeek (10x10 sliver), computed from the REAL
primary screen bounds (GetSystemMetrics SM_CXSCREEN/SM_CYSCREEN — the same
values as System.Windows.Forms.Screen.PrimaryScreen.Bounds without the
WinForms dependency; fallback 1920x1080).

STEP 3: every hide path — ChromeWindowMinimizer.HideAsync (post-login/
early-hide) and ChromeWindowController.HideAsync (keyless hide / WPF /
peek return) — now moves to the same tiny 80x80 corner square with
windowState:normal instead of -32000,-32000, verified by a full
getWindowBounds read-back.

DEFAULT FLIP: WindowMode default is now 'TinyCorner' per the task's
conditional — Offscreen is proven broken (turn-17) with the flag present.
docs/configuration.md documents the visible-on-screen >=80x80 rule.

STEP 5: 'keyless diagnostics chrome <workspace>' prints every running
chrome.exe command line (Get-CimInstance Win32_Process via PowerShell),
the disable-features/window-position lines, and a verdict on
CalculateNativeWinOcclusion presence.

Tests: TinyCorner/EdgePeek argument theory (explicit 1920x1080), screen
bounds non-hardcoded, chrome-launched wiring through the connector,
controller hide bounds = tiny corner. 611 core + 208 WPF tests green.
GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN) returns the FULL monitor
resolution, so the 80x80 corner square's bottom edge landed exactly at the
physical screen corner — under the always-on-top taskbar: technically
on-screen, actually invisible, and not distinguishable in the taskbar
either.

PrimaryScreenSize() now uses SystemParametersInfo(SPI_GETWORKAREA), which
returns the primary monitor's work area RECT (taskbar excluded), keeping
the (int Width, int Height) shape and the 1920x1080 fallback. Both
consumers — TinyCornerBounds() and WindowModeFlag()/ComposeChromeArguments()
(tinycorner/edgepeek) — feed from it unchanged, so the launch flags and the
CDP hide/show geometry always agree and stay inside the visible desktop.
TinyCornerBounds' public signature is unchanged; only what feeds it changed.
Parameter names now say workAreaWidth/workAreaHeight.

Tests: with a fabricated 48px taskbar (work area 1920x1032) the square's
edges satisfy Top+Height <= 1032 and Left+Width <= 1920; without an offset
(1920x1080) it returns exactly (1840,1000,80,80). 613 core + 208 WPF tests
green. Live check pending: chrome-launched event shows work-area-based
--window-position, and the 80x80 square is visible on the desktop during a
turn.
…onest failures

IChromeWindowController (keyless show/hide, WPF buttons, login flow) was a
weaker duplicate of ChromeWindowMinimizer: one blind setWindowBounds, no
read-back, no Win32 fallback, wrong window with several tabs open, and
false successes.

ChromeWindowMover (new, internal static) is now the single engine:
- TryMoveViaCdpAsync: setWindowBounds + Browser.getWindowBounds read-back
  compared against the EXACT target bounds/windowState, forced-normal
  recovery between attempts, up to 3 tries.
- ShowWindowsViaWin32: EnumWindows over OUR chrome.exe processes (launched
  PID tree or --user-data-dir/--remote-debugging-port command-line match),
  ShowWindow with any command, then a VISIBILITY read-back (IsWindowVisible/
  IsIconic) — works for BOTH directions now (hide: SW_HIDE; show:
  SW_SHOWNOACTIVATE so no focus steal). A test-only override hook keeps
  unit tests away from real windows.
- PickChatPage: prefers the page whose URL is chat.deepseek.com, falls back
  to FirstOrDefault, null lets the caller create one.

ChromeWindowMinimizer keeps its public behavior (best-effort, never fails a
turn) and orchestrates the mover with the 5s CDP race. ChromeWindowController
now verifies (3-attempt read-back), falls back to Win32 in BOTH directions,
picks the chat page, and returns Outcome.Failure 'Chrome window could not be
located or moved - it may have been closed.' when neither path can verify —
no more false 'shown'/'hidden' notes. Win32 hide logic that used to be
private in the minimizer moved to the mover; nothing duplicated.

Tests: the CDP fake now echoes applied bounds (read-back semantics) with
mismatch/throw switches; new tests for the honest-failure message, the
Win32-rescued success, and PickChatPage preference. 615 core + 208 WPF
tests green.
… defaults again

CreateDefault() serializes every field explicitly, so once a workspace's
keyless.json reaches the current SchemaVersion, MigrateIfStaleAsync never
runs again - and a literal like "WindowMode": "Minimized" written under an
old shipped default deserializes forever, silently defeating every later
default fix (the exact reason Offscreen/Minimized kept truncating turns).

ConfigurationService now upgrades known-stale default literals on EVERY
load, orthogonal to the SchemaVersion-gated migration:
- KnownStaleDefaults: a single static table of (FieldPath, OldDefault,
  NewDefault, Read, With) entries - one line per future default change,
  with a comment spelling out the pattern. Seeded with Browser.WindowMode
  Minimized->TinyCorner and Offscreen->TinyCorner.
- Only values EXACTLY matching a known old default are rewritten (strings
  OrdinalIgnoreCase, so case variants of a shipped default count as the
  default, not a customization); anything else is left untouched.
- Upgrades back up the raw file as keyless.json.pre-defaults-upgrade.bak
  (cannot collide with the real .v{N}.bak schema backups), rewrite the file
  with the upgraded options (all other loaded values - user
  customizations - survive), and log one informational line per field.
- LastLoadUpgradedFields (concrete property) exposes what fired for tests
  and diagnostics; ConfigurationService gains an optional ILoggerFactory
  (factory CreateConfigurationService(loggerFactory) passes one through).

Tests: Minimized literal at current SchemaVersion -> TinyCorner + backup +
log line; Offscreen -> TinyCorner; 'Normal' untouched (no backup, no
rewrite); idempotent second load (no re-upgrade, backup preserved); the
rewrite preserves user customizations (ReplyTimeoutSeconds 120, Mode Auto).
620 core + 208 WPF tests green.
…electors fixed

selectors.json had NO versioning: written once from the embedded default,
read verbatim forever - so improved MessageCopyButton selectors never
reached existing workspaces and ReplySource=Auto permanently fell back to
Dom via the circuit breaker.

- Embedded default carries CatalogVersion: 2; SelectorsCatalog exposes
  CatalogVersion (absent = 0 = pre-versioning file).
- SelectorsCatalog.MergeAsync: when the file's version is older (or forced),
  back up as selectors.json.v<N>.bak, then MERGE - a key whose workspace
  value matches ANY version ever shipped for it (ShippedValues table,
  generated from the resource's git history) is refreshed to the current
  embedded value; genuine customizations and workspace-only keys survive;
  keys added by later releases are picked up. Key matching is case-
  insensitive (tolerant parser); comparisons canonical through
  System.Text.Json so hand-reformatting still compares equal. One
  informational log names every refreshed key (provider passes its logger
  factory).
- keyless selectors refresh <workspace>: forces the merge unconditionally
  with the same backup, printing exactly which keys were refreshed.
- The MessageCopyButton miss warning now points at
  'keyless selectors refresh' as the first remedy.

Tests: old-version file (no CatalogVersion + old MessageCopyButton + old
shipped SendButton + hand-edited InputBox) -> copy-button/SendButton
refreshed to embedded, InputBox preserved, backup written, log line present;
current file byte-identical and no backup; forced refresh no-ops with a
backup; missing keys added from the embedded default. 624 core + 208 WPF
tests green.
The fixed 20-attempt cap burned out in ~2-4s: each truncation retry
continued without the poll delay, so detections ran back-to-back (~150ms
apart) while real throttled streams took 8+ seconds to resume - a stream
that eventually succeeded was killed anyway.

- New BrowserOptions.TruncationBudgetSeconds (default 20, validated >= 5):
  WaitForReplyAsync tracks the time since the FIRST truncation detection
  and fails with KA_REPLY_TRUNCATED only once that budget is exceeded.
  Failure message names the elapsed seconds: 'Reply stream was cut off
  repeatedly for {N}s in this turn. ...'
- The truncation branch now awaits the same Task.Delay(_pollDelayMs) as
  the normal poll path - no more busy-looping.
- The reply-truncated turn event gains ElapsedSeconds (real timing in
  turn-N.jsonl); the Warning keeps its format.
- AutoFocusOnTruncation still fires once, but at 25% of the budget
  (proportional) instead of a hardcoded attempt 5.
- Catalog/doc wording updated to the time-based semantics.

Tests rewritten for the budget semantics (1s/8s budgets to keep them fast:
>=10 spaced detections, ElapsedSeconds ascending and past the budget,
focus at >= quarter, exactly one post-focus detection). 624 core + 208 WPF
tests green across repeated runs.
A live chrome-launched log showed --window-position=1840,1000 (raw screen)
despite the SPI_GETWORKAREA fix: the process was not DPI-aware, so at a
non-100% display scale GetSystemMetrics/SPI_GETWORKAREA return
OS-VIRTUALIZED pixel values that do not match the physical grid a
DPI-aware Chrome renders into.

1. DPI awareness: the CLI embeds app.manifest with
   <dpiAwareness>PerMonitorV2</dpiAwareness> (ApplicationManifest wired in
   the csproj - the CLI is the process that launches Chrome and computes
   the geometry; verified the manifest is embedded in the built exe); the
   WPF csproj sets <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
   for the Show/Hide buttons' host.
2. Every Chrome launch logs raw screen size vs work area side by side
   (BrowserEnvironment.RawScreenSize is back), and the chrome-launched turn
   event gains RawScreenSize/WorkAreaSize fields - future discrepancies are
   visible in turn logs without manual reproduction.
3. Post-launch verification: after the launch attach, the connector locates
   the just-launched window's real HWND (ChromeWindowMover.FindOurChromeWindows
   made internal, polled up to 3s because the port can open before the window
   exists), GetWindowRects it (ChromeWindowMover.GetWindowBoundsWin32), and
   logs actual-vs-requested against RequestedWindowBounds(mode) with an 8px
   tolerance - a MISMATCH warning + chrome-window-bounds event expose Chrome
   silently clamping/repositioning the window (e.g. minimum-size enforcement),
   a root cause previously invisible. Read-only; guarded off on the test seam.

Tests: RequestedWindowBounds theory (TinyCorner/EdgePeek/Offscreen/Minimized/
Normal-null), RawScreenSize sanity, chrome-launched payload carries both size
fields. 630 core + 208 WPF tests green.
…ded nothing)

Verified at the binary level: <ApplicationHighDpiMode>PerMonitorV2</...>
left ZERO PerMonitorV2 bytes in the built WPF exe, while the CLI's explicit
app.manifest approach embedded it. The WPF csproj now uses the same
guaranteed mechanism (app.manifest + ApplicationManifest); re-verified the
setting is present in the exe. WPF tests green.
…p.OnExit net

There was no way to truly CLOSE the Chrome the agent launched - only
Show/Hide window moves - and the TinyCorner window is invisible by design,
so it could silently outlive the session.

IChromeWindowController.CloseAsync(options, allowAttached = false):
- ownership matched from the RUNNING processes' command lines via
  ChromeWindowMover.GetChromeProcessInfos (extracted; CollectOurChromeProcessIds
  now internal and rebuilt on top of it) - profile-dir match = ours-by-profile,
  port-only match = the user's own browser on our port;
- a Chrome matched only by the port is REFUSED unless allowAttached (the
  AttachedToRunningChrome ownership rule; message says it was NOT closed);
- graceful CDP Browser.close first (whole browser, profile flushed), then the
  debug port polled (~3s) to confirm it actually went down;
- only if it stayed up: Process.Kill(entireProcessTree: true) on every owned
  root pid - helper processes included - then a final port+process check;
- honest KA_BROWSER_CONNECTION_FAILED failure when even the kill cannot be
  verified. Test seams (ProcessInfosOverride/KillOverride) keep unit tests
  away from live processes and real kills.

WPF: Close Chrome button on the Doctor page (4th ChromeViewModel command,
same IsBusy cross-disabling, IsClosing flag); a successful close clears the
stale Chrome-ready/visible status and notes 'Chrome closed. Use Start Chrome
to open it again.'

App.OnExit: ALWAYS-ON safety net closing the Chrome the app itself launched
on normal exit - bounded to 4s (worker task + ManualResetEventSlim wait, no
sync-over-async: the NoSyncOverAsync rule forbids GetResult/.Result/.Wait),
never lets an exception escape. Browser.CloseChromeOnExit keeps its exact
meaning (per-turn session disposal); both sites carry comments explaining the
distinction.

Tests: graceful close confirmed by port-down; kill fallback reached when the
port stays up (kill recorded via the seam) with honest failure when it cannot
verify; attached-only Chrome refused without any CDP traffic and allowed with
the explicit flag; nothing-running failure; WPF close-command success/failure/
disabled. 635 core + 211 WPF tests green.
… caps

Large workspaces still hit DeepSeek's length limit: the default mode was
Full, the TreeAndChanged no-baseline fallback attached the same unlimited
full dump, ChunkDump only capped per-part size (five 240k parts still
arrived together), and nothing filtered huge generated files by name or size.

A) SnapshotOptions.Mode defaults to TreeAndChanged (Full fully supported).
B) SnapshotOptions.MaxTotalAttachmentChars (400,000 = chars/4 tokens): when
   tree + dump exceed it, the tree stays complete and the dump keeps only
   files REFERENCED by the turn's request text (file name or path,
   case-insensitive); everything else becomes a per-folder omission note
   ('[omitted: N files under src/Core/ (182 KB) - use the file-read tool…').
   A snapshot-budget-exceeded turn event records the original total, the
   ceiling and the omissions. BuildAttachmentsAsync gained requestText +
   logEvent parameters (wired from AgentLoop; feedback path too).
C) The TreeAndChanged no-baseline/empty-diff fallback routes through the
   same budget path - a first turn is never an unlimited full dump. Also
   fixed a pre-existing flaw: the changed-files diff was computed AFTER the
   baseline was overwritten (always vacuously empty), so it is now stashed
   at refresh time (.agent/snapshot-changes.json) BEFORE the baseline update
   - the second turn's changed-files.txt finally lists real changes.
D) Snapshot.Filter.ExcludeFilePatterns (min.js/min.css/lock files/designer/
   g.cs/ModelSnapshot wildcards) drop generated files from the attached dump;
   Snapshot.MaxDumpedFileChars (50,000) replaces oversized files with their
   heading + '[file too large to attach: N KB - use the file-read tool…]'.
   Disk copies stay untouched in both cases.

Tests: budget keeps tree + referenced files with the omission note and the
turn event; second turn uses the stashed diff; oversized file shows heading +
placeholder; denylisted files vanish from the attachment but not from disk;
small workspaces behave exactly as before. 641 core + 211 WPF tests green.
…ject READMEs

The DepLens publishing flow is ported: publish.bat in the CLI root runs
publish.ps1 from the solution root, which packs the CLI into
artifacts/ImanSoftware.KeylessAgent.Cli (auto-bumping the patch version in
Directory.Build.props when an artifact already exists) and pushes the
nupkg + snupkg to NuGet.org with a 30s timeout, cleaning up on failure.

- Icons: the attached logo (665x665 RGBA PNG) goes to icon.png in both the
  CLI and the WPF project (PackageIcon); the WPF exe also gets icon.ico
  (256x256 PNG-in-ICO, decoded/resized/re-encoded in pure python - no PIL
  on this machine) wired as <ApplicationIcon>.
- Packaging metadata (Directory.Build.props already carried the Authors/
  Company/Copyright/MIT/URL/symbols/PackageReadmeFile block): the CLI csproj
  adds PackAsTool + ToolCommandName keyless + PackageId
  ImanSoftware.KeylessAgent.Cli + Description + tags + PackageIcon + the
  icon/readme pack items. The WPF csproj adds the same pack items. Per this
  repo's test-enforced convention the <Version> stays ONLY in
  Directory.Build.props, so publish.ps1 was adapted to bump it THERE (not
  the csproj like DepLens) - the VersionMatchesDirectoryBuildProps test
  stays green.
- READMEs: the CLI README gains a full command reference (run/resume/undo/
  config/chrome/doctor/show/hide/diagnostics chrome/selectors refresh with
  examples, interactive slash commands, Ctrl+C, exit codes) plus the
  versioning/publishing section; the WPF project gets its own README
  (pages, Chrome window control incl. Close Chrome, requirements); the
  root README (319 lines, written from the final code) remains the general
  project documentation and keeps every token the consistency tests require.

Verified: dotnet pack produces ImanSoftware.KeylessAgent.Cli.1.0.0.nupkg +
snupkg with icon.png/README.md/keyless tool metadata inside; manifest +
DPI settings intact; 641 core + 211 WPF tests green. First real publish
needs NUGET_API_KEY (setx NUGET_API_KEY ...).
1. icon.ico rebuilt WITHOUT any hand-written pixel code: a one-off WPF
   converter decodes icon.png, resizes to 256x256 with the SDK's
   TransformedBitmap/PngBitmapEncoder, and the script only wraps those
   encoder-produced PNG bytes verbatim in the documented 6+16-byte
   PNG-compressed ICO entry (planes=1, bpp=32). Verified VISUALLY: the .ico
   was decoded back with IconBitmapDecoder and rendered to a PNG - clean
   cyan-on-black diamond logo, colors and shape identical to icon.png, no
   scrambling (the broken render came from the old hand-rolled decode/
   re-encode path, now deleted).
2. WPF csproj: <IsPackable>false</IsPackable> (the pack items from the
   previous round removed as dead weight). Verified: dotnet pack on the WPF
   project produces NO package content.
3. publish.ps1 version-bump path exercised end-to-end WITHOUT a real push
   (-SkipPush, dummy NUGET_API_KEY): run 1 packed 1.0.0; run 2 detected the
   1.0.0 artifact, auto-bumped 1.0.0 -> 1.0.1 in Directory.Build.props
   (csproj untouched - zero <Version> elements there), packed 1.0.1, and the
   summary shows 'Directory.Build.props updated'. DBP restored to 1.0.0
   afterwards, artifacts/ gitignored (already) and the local artifacts
   folder removed; VersionMatchesDirectoryBuildProps passes and the full
   suites stay green (641 core + 211 WPF).
main's PR #1 was a squash of an EARLIER snapshot of this same branch, so
every overlapping file conflicted add/add; each resolves to the feature
version (the newest state - main's README, for example, is byte-identical
to the feature's pre-restructure copy). Everything main added that the
feature never touched (.editorconfig, .gitattributes, .github workflow,
global.json, nuget.config, LICENSE.txt, Directory.Packages.props,
src/Playground) carries over unchanged.

Merged tree: build clean, 641 core + 211 WPF tests green.
@ImanEstiri
ImanEstiri merged commit 782b31f into main Nov 5, 2026
1 check failed
@ImanEstiri
ImanEstiri deleted the feature/keyless-agent branch November 13, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant