@yagni-app/code-staging 0.1.0-staging.1015.1 → 0.1.0-staging.1017.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -139,9 +139,12 @@ yagni use prod # switch back (sticky); prod is the def
139
139
 
140
140
  ## Configuration
141
141
 
142
- | Variable | Default | Purpose |
143
- | ---------------- | ------------------------ | -------------------------------------------------- |
144
- | `YAGNI_BASE_URL` | active environment's URL | Override the base URL for a **single run** (escape hatch). Prefer `yagni use` for anything sticky. |
142
+ | Variable | Default | Purpose |
143
+ | ----------------------------- | ------------------------ | -------------------------------------------------- |
144
+ | `YAGNI_BASE_URL` | active environment's URL | Override the base URL for a **single run** (escape hatch). Prefer `yagni use` for anything sticky. |
145
+ | `YAGNI_DISABLE_BRANDING` | unset | `1` skips the YAGNI Code system-prompt rewrite entirely, so the engine's assembled prompt passes through byte-exact (no identity swap, no company-brief injection). |
146
+ | `YAGNI_DISABLE_UPDATE_CHECK` | unset | `1` silences the new-version notice and the background update check. |
147
+ | `YAGNI_DISABLE_CLAUDE_COMPAT` | unset | `1` turns off the zero-config `.claude` assets bridge (skills and commands). |
145
148
 
146
149
  Credentials live in `~/.yagni-code/profiles/<name>.json` (mode `0600`); the active
147
150
  environment is recorded in `~/.yagni-code/config.json`. A pre-profiles
package/dist/cli.js CHANGED
@@ -176,6 +176,7 @@ export const HELP_TEXT = [
176
176
  "Set YAGNI_BASE_URL to override the base URL for a single run.",
177
177
  "Set YAGNI_DISABLE_UPDATE_CHECK=1 to silence the new-version notice.",
178
178
  "Set YAGNI_DISABLE_CLAUDE_COMPAT=1 to skip loading .claude assets.",
179
+ "Set YAGNI_DISABLE_BRANDING=1 to pass the system prompt through unmodified.",
179
180
  "Set YAGNI_DISABLE_CRASH_REPORTS=1 to turn off sanitized crash reports.",
180
181
  ].join("\n");
181
182
  /** Parse `use <name> [--base-url <url>]` argv into its parts. */
package/dist/doctor.d.ts CHANGED
@@ -60,6 +60,13 @@ export declare function checkCliUpdate(probe: {
60
60
  latest: string | null;
61
61
  }): CheckResult;
62
62
  export declare function checkGh(onPath: boolean): CheckResult;
63
+ /** What the Windows bash probe found (pi needs a bash — Git Bash — on win32). */
64
+ export interface BashProbe {
65
+ found: boolean;
66
+ /** The resolved bash path, when found. */
67
+ where?: string;
68
+ }
69
+ export declare function checkBash(probe: BashProbe): CheckResult;
63
70
  export interface DoctorReport {
64
71
  checks: CheckResult[];
65
72
  exitCode: number;
@@ -75,12 +82,26 @@ export interface DoctorDeps {
75
82
  probeBackend?: (baseUrl: string, token: string) => Promise<BackendProbe>;
76
83
  probeStateDir?: () => StateDirProbe;
77
84
  ghOnPath?: () => boolean;
85
+ /** Platform seam for the win32-only bash check (defaults to process.platform). */
86
+ platform?: NodeJS.Platform;
87
+ /** Windows bash probe; only ever called when the platform is win32. */
88
+ probeBash?: () => BashProbe;
78
89
  currentVersion?: string;
79
90
  probeLatestVersion?: () => Promise<string | null>;
80
91
  log?: (msg: string) => void;
81
92
  }
82
93
  /** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
83
94
  export declare function ghOnPathDefault(env?: NodeJS.ProcessEnv): boolean;
95
+ /**
96
+ * Locate the bash pi will actually use on Windows. The order and locations
97
+ * MIRROR pi 0.83's own shell resolution (dist/utils/shell.js) exactly:
98
+ * `%ProgramFiles%\Git\bin\bash.exe`, then `%ProgramFiles(x86)%\Git\bin\bash.exe`,
99
+ * then `bash.exe` on PATH (`where bash.exe`). Deliberately NOTHING wider — a
100
+ * per-user Git install in `%LOCALAPPDATA%` that is not on PATH is invisible
101
+ * to pi, and a doctor that reported it green would bless a machine where the
102
+ * first bash tool call throws. Pure function of env, like the gh probe.
103
+ */
104
+ export declare function bashOnWindowsDefault(env?: NodeJS.ProcessEnv): BashProbe;
84
105
  /**
85
106
  * Gather every check result against the (injectable) probes. Pure ordering; each
86
107
  * individual check is a pure function of its probe.
package/dist/doctor.js CHANGED
@@ -198,6 +198,24 @@ export function checkGh(onPath) {
198
198
  required: false,
199
199
  };
200
200
  }
201
+ export function checkBash(probe) {
202
+ if (!probe.found) {
203
+ return {
204
+ name: "bash",
205
+ status: "fail",
206
+ detail: "no bash found (pi runs its shell commands through bash)",
207
+ hint: "Install Git for Windows — pi needs its bash: https://gitforwindows.org "
208
+ + "(a per-user install must also put bash.exe on PATH)",
209
+ required: true,
210
+ };
211
+ }
212
+ return {
213
+ name: "bash",
214
+ status: "ok",
215
+ detail: probe.where ? `found (${probe.where})` : "found",
216
+ required: true,
217
+ };
218
+ }
201
219
  function toOctal(mode) {
202
220
  return `0${(mode & 0o777).toString(8).padStart(3, "0")}`;
203
221
  }
@@ -296,6 +314,32 @@ export function ghOnPathDefault(env = process.env) {
296
314
  }
297
315
  return false;
298
316
  }
317
+ /**
318
+ * Locate the bash pi will actually use on Windows. The order and locations
319
+ * MIRROR pi 0.83's own shell resolution (dist/utils/shell.js) exactly:
320
+ * `%ProgramFiles%\Git\bin\bash.exe`, then `%ProgramFiles(x86)%\Git\bin\bash.exe`,
321
+ * then `bash.exe` on PATH (`where bash.exe`). Deliberately NOTHING wider — a
322
+ * per-user Git install in `%LOCALAPPDATA%` that is not on PATH is invisible
323
+ * to pi, and a doctor that reported it green would bless a machine where the
324
+ * first bash tool call throws. Pure function of env, like the gh probe.
325
+ */
326
+ export function bashOnWindowsDefault(env = process.env) {
327
+ for (const root of [env.ProgramFiles, env["ProgramFiles(x86)"]]) {
328
+ if (!root)
329
+ continue;
330
+ const candidate = join(root, "Git", "bin", "bash.exe");
331
+ if (existsSync(candidate))
332
+ return { found: true, where: candidate };
333
+ }
334
+ for (const dir of (env.PATH ?? "").split(delimiter)) {
335
+ if (!dir)
336
+ continue;
337
+ const candidate = join(dir, "bash.exe");
338
+ if (existsSync(candidate))
339
+ return { found: true, where: candidate };
340
+ }
341
+ return { found: false };
342
+ }
299
343
  /**
300
344
  * Gather every check result against the (injectable) probes. Pure ordering; each
301
345
  * individual check is a pure function of its probe.
@@ -308,10 +352,18 @@ export async function gatherChecks(deps = {}) {
308
352
  const probeBackend = deps.probeBackend ?? defaultProbeBackend;
309
353
  const probeStateDir = deps.probeStateDir ?? defaultProbeStateDir;
310
354
  const ghOnPath = deps.ghOnPath ?? (() => ghOnPathDefault());
355
+ const platform = deps.platform ?? process.platform;
356
+ const probeBash = deps.probeBash ?? (() => bashOnWindowsDefault());
311
357
  const probeLatestVersion = deps.probeLatestVersion ?? (() => fetchLatestVersion());
312
358
  const checks = [];
313
359
  checks.push(checkPiEngine(probePiEngine()));
314
360
  checks.push(checkExtension(probeExtension()));
361
+ // win32 only, and skipped means NOT SHOWN: on macOS/Linux there is nothing
362
+ // to say. pi shells out through bash, so a Windows machine without Git Bash
363
+ // cannot launch at all — a required red, like a missing engine.
364
+ if (platform === "win32") {
365
+ checks.push(checkBash(probeBash()));
366
+ }
315
367
  checks.push(checkCliUpdate({
316
368
  current: deps.currentVersion ?? currentCliVersion(),
317
369
  latest: await probeLatestVersion(),
@@ -17,6 +17,14 @@
17
17
  * one (and vice versa). Everything is pure except the in-memory rule list.
18
18
  */
19
19
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
20
+ /**
21
+ * Windows-only separator normalization so prefixes compare and display with
22
+ * `/` on every platform: pi's tools emit forward-slash paths even on Windows,
23
+ * and a rule keyed `C:\repo\src\api` would silently never match a call for
24
+ * `C:/repo/src/api/a.ts`. On POSIX this is the identity (a `\` there is a
25
+ * legal filename character, not a separator).
26
+ */
27
+ const norm = sep === "\\" ? (p) => p.split("\\").join("/") : (p) => p;
20
28
  /** The file path a call targets, or null for path-less tools (bash). */
21
29
  export function blessPath(params) {
22
30
  const p = params.path;
@@ -25,7 +33,12 @@ export function blessPath(params) {
25
33
  /** Build a fresh, empty session bless store rooted at `cwd`. */
26
34
  export function makeBlessStore(cwd) {
27
35
  const rules = [];
28
- const abs = (p) => (isAbsolute(p) ? p : resolve(cwd, p));
36
+ // Always THROUGH resolve, even for absolute inputs: on Windows a bare
37
+ // "/repo/…" is drive-relative and resolve() drive-qualifies it, so a rule
38
+ // minted from a relative path and a call carrying an absolute one land on
39
+ // the same canonical form. (For an already-absolute POSIX path this is just
40
+ // normalization.)
41
+ const abs = (p) => norm(resolve(cwd, p));
29
42
  /** Absolute directory prefix a bless of this call would cover, or null. */
30
43
  function prefixFor(params) {
31
44
  const p = blessPath(params);
@@ -41,7 +54,7 @@ export function makeBlessStore(cwd) {
41
54
  const prefix = prefixFor(params);
42
55
  if (prefix === null)
43
56
  return null;
44
- const rel = relative(cwd, prefix);
57
+ const rel = norm(relative(cwd, prefix));
45
58
  // Inside the tree → the relative dir (or "." for the repo root); outside →
46
59
  // the absolute path so the user sees exactly what they are blessing.
47
60
  if (rel === "")
@@ -64,7 +77,7 @@ export function makeBlessStore(cwd) {
64
77
  if (p === null)
65
78
  return false; // path-less (bash) never auto-approves
66
79
  const target = abs(p);
67
- return rules.some((r) => r.tool === tool && (target === r.prefix || target.startsWith(r.prefix + sep)));
80
+ return rules.some((r) => r.tool === tool && (target === r.prefix || target.startsWith(`${r.prefix}/`)));
68
81
  },
69
82
  rules() {
70
83
  return rules.slice();
@@ -17,6 +17,15 @@ export declare const BRAND_NAME = "YAGNI Code";
17
17
  */
18
18
  export declare const YAGNI_IDENTITY: string;
19
19
  export declare const PI_IDENTITY_RE: RegExp;
20
+ /**
21
+ * Env switch that bypasses the system-prompt rewrite entirely, so pi's
22
+ * assembled prompt passes through byte-exact (no identity swap, no scrub, no
23
+ * brief injection, no closing reminder). Same predicate as the sibling
24
+ * switches YAGNI_DISABLE_UPDATE_CHECK / YAGNI_DISABLE_CLAUDE_COMPAT.
25
+ */
26
+ export declare const BRANDING_DISABLE_ENV = "YAGNI_DISABLE_BRANDING";
27
+ /** `"1"`/anything truthy disables; unset, empty, and `"0"` keep branding on. */
28
+ export declare function brandingDisabled(env: NodeJS.ProcessEnv): boolean;
20
29
  export interface BrandSystemPromptOptions {
21
30
  /** Override the identity paragraph (defaults to {@link YAGNI_IDENTITY}). */
22
31
  identity?: string;
@@ -56,6 +56,18 @@ function scrubOutsideProjectContext(s) {
56
56
  .map((p) => (p.startsWith("<project_context>") ? p : scrubPiHarness(p)))
57
57
  .join("");
58
58
  }
59
+ /**
60
+ * Env switch that bypasses the system-prompt rewrite entirely, so pi's
61
+ * assembled prompt passes through byte-exact (no identity swap, no scrub, no
62
+ * brief injection, no closing reminder). Same predicate as the sibling
63
+ * switches YAGNI_DISABLE_UPDATE_CHECK / YAGNI_DISABLE_CLAUDE_COMPAT.
64
+ */
65
+ export const BRANDING_DISABLE_ENV = "YAGNI_DISABLE_BRANDING";
66
+ /** `"1"`/anything truthy disables; unset, empty, and `"0"` keep branding on. */
67
+ export function brandingDisabled(env) {
68
+ const value = env[BRANDING_DISABLE_ENV];
69
+ return value !== undefined && value !== "" && value !== "0";
70
+ }
59
71
  /**
60
72
  * Rebrand pi's assembled system prompt as YAGNI Code's, and optionally inject a
61
73
  * live company brief.
@@ -26,7 +26,7 @@ import { matchesKey } from "@earendil-works/pi-tui";
26
26
  import { spawnSync } from "node:child_process";
27
27
  import { readFileSync, unlinkSync, existsSync } from "node:fs";
28
28
  import { tmpdir } from "node:os";
29
- import { join, basename } from "node:path";
29
+ import { join, basename, isAbsolute } from "node:path";
30
30
  import { randomUUID } from "node:crypto";
31
31
  import { logImagePaste } from "./diagnostics.js";
32
32
  /** Matches the `[Image #N]` chip token in the editor text. */
@@ -81,10 +81,14 @@ export function readPastedImagePath(pastedText) {
81
81
  // must confirm the un-escaped form is a well-formed tmp image path before any
82
82
  // unescaping or path resolution. cmux tmp paths contain no escapable chars
83
83
  // (no spaces/metacharacters), so a legitimate paste has NO backslashes at all.
84
- if (trimmed.includes("\\"))
84
+ // On Windows the backslash IS the path separator (and cmd/PowerShell do no
85
+ // backslash-escaping), so the escape-smuggling rejection applies only where
86
+ // a backslash could be an escape: POSIX shells.
87
+ const isWindows = process.platform === "win32";
88
+ if (!isWindows && trimmed.includes("\\"))
85
89
  return null; // escapes => not a plain cmux tmp path
86
90
  const path = trimmed;
87
- if (!path.startsWith("/"))
91
+ if (isWindows ? !isAbsolute(path) : !path.startsWith("/"))
88
92
  return null;
89
93
  const name = basename(path);
90
94
  if (!name.startsWith("clipboard-"))
@@ -6,7 +6,7 @@ import { makeReviewBusinessMatchTool } from "./reviewTool.js";
6
6
  import { makeRecordEngineeringContextTool } from "./recordContextTool.js";
7
7
  import { makeRecordDecisionTool } from "./recordDecisionTool.js";
8
8
  import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
9
- import { BRAND_NAME, brandSystemPrompt, buildMastheadString } from "./branding.js";
9
+ import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString } from "./branding.js";
10
10
  import { appendClaudeRules, claudeRulesSection } from "./claudeRules.js";
11
11
  import { registerCostCommand } from "./costHud.js";
12
12
  import { isFreshWorkspace, runInitPass as defaultRunInitPass } from "./initPass.js";
@@ -238,10 +238,16 @@ export async function registerYagni(pi, deps = {}) {
238
238
  // pointers. Computed once per activation (rules are launch-time state, like
239
239
  // pi's own skill discovery); fail-soft to null.
240
240
  const rulesSection = claudeRulesSection(deps.env ?? process.env);
241
- // Own the identity + inject live company context (and repo rules) on every turn.
242
- pi.on("before_agent_start", (event) => ({
243
- systemPrompt: appendClaudeRules(brandSystemPrompt(event.systemPrompt, { contextBrief }), rulesSection),
244
- }));
241
+ // Own the identity + inject live company context (and repo rules) on every
242
+ // turn. Under YAGNI_DISABLE_BRANDING the handler returns nothing, so pi's
243
+ // assembled prompt passes through byte-exact (no rewrite, no brief
244
+ // injection, no rules section).
245
+ const noBranding = brandingDisabled(deps.env ?? process.env);
246
+ pi.on("before_agent_start", (event) => noBranding
247
+ ? undefined
248
+ : {
249
+ systemPrompt: appendClaudeRules(brandSystemPrompt(event.systemPrompt, { contextBrief }), rulesSection),
250
+ });
245
251
  // Two finalized-message guards share this handler (their conditions are
246
252
  // mutually exclusive: YAG-460 takes error-stopped messages, YAG-466 takes
247
253
  // stop/length ones without an errorMessage).
@@ -46,7 +46,12 @@
46
46
  */
47
47
  import { execFile } from "node:child_process";
48
48
  import { readFileSync } from "node:fs";
49
- import { basename, dirname, join } from "node:path";
49
+ // posix path math on purpose, on every platform: the rel paths here come from
50
+ // git porcelain output, which is forward-slash even on Windows, and Windows fs
51
+ // APIs accept forward-slash paths — while win32 join/dirname would emit
52
+ // backslashed strings that no longer compare equal to the git-derived repoRoot
53
+ // and would break the walk-up termination check.
54
+ import { basename, dirname, join } from "node:path/posix";
50
55
  import { composeAbortSignal } from "./resilience.js";
51
56
  import { scrubSecrets } from "./scrubSecrets.js";
52
57
  import { snapshotWorkspace } from "./workspace.js";
@@ -25,7 +25,9 @@
25
25
  */
26
26
  import { execFile } from "node:child_process";
27
27
  import { mkdirSync, readFileSync } from "node:fs";
28
- import { dirname, join } from "node:path";
28
+ // posix join on purpose (see verify.ts): worktree paths are composed from
29
+ // git output and compared as strings; Windows fs APIs accept forward slashes.
30
+ import { dirname, join } from "node:path/posix";
29
31
  import { composeAbortSignal } from "./resilience.js";
30
32
  import { buildVerifyEnv, detectPackageManager } from "./verify.js";
31
33
  /** Wall-clock cap on the best-effort worktree bootstrap install (spec §3b). */
@@ -22,7 +22,7 @@
22
22
  * - skipped entirely when the corpus is thin (<3 active decisions) or eval mode.
23
23
  * - any failure degrades to no injection.
24
24
  */
25
- import { isAbsolute, relative } from "node:path";
25
+ import { isAbsolute, relative, sep } from "node:path";
26
26
  import { composeAbortSignal } from "./pipeline/resilience.js";
27
27
  /** Recall must never block a read beyond this (spec §4). */
28
28
  export const RECALL_TIMEOUT_MS = 1500;
@@ -60,7 +60,10 @@ export function normalizeRecall(data) {
60
60
  export function toRepoRelativePath(readPath, cwd) {
61
61
  let p = readPath.trim();
62
62
  if (isAbsolute(p)) {
63
- const rel = relative(cwd, p);
63
+ // The backend keys judgments by forward-slash repo paths, so the rebased
64
+ // form must not carry win32 separators (also keeps the per-path session
65
+ // cache keyed identically across platforms).
66
+ const rel = relative(cwd, p).split(sep).join("/");
64
67
  if (rel && !rel.startsWith("..") && !isAbsolute(rel))
65
68
  p = rel;
66
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "0.1.0-staging.1015.1",
3
+ "version": "0.1.0-staging.1017.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -38,5 +38,5 @@
38
38
  "@earendil-works/pi-tui": "0.83.0",
39
39
  "typebox": "^1.1.38"
40
40
  },
41
- "yagniSourceSha": "eac9ab13212b31799bf860cd83d39dcb325e5c39"
41
+ "yagniSourceSha": "c52a9bdcb42c50177c0d753f839fe884cc96c784"
42
42
  }