@yagni-app/code-staging 1.1.1-staging.1361.1 → 1.1.2-staging.1364.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
@@ -201,6 +201,7 @@ yagni use prod # switch back (sticky); prod is the def
201
201
  | `YAGNI_DISABLE_UPDATE_CHECK` | unset | `1` silences the new-version notice and the background update check. |
202
202
  | `YAGNI_DISABLE_CLAUDE_COMPAT` | unset | `1` turns off the zero-config `.claude` assets bridge (skills and commands). |
203
203
  | `YAGNI_DISABLE_CRASH_REPORTS` | unset | `1` disables crash reporting (see "What leaves your machine"). |
204
+ | `YAGNI_SYSTEM_CA` | unset | `1` makes sessions trust your OS certificate store; `0` also disables the automatic retry (see "Behind a corporate TLS proxy"). |
204
205
 
205
206
  Credentials live in `~/.yagni-code/profiles/<name>.json` (mode `0600`); the active
206
207
  environment is recorded in `~/.yagni-code/config.json`. A pre-profiles
@@ -357,6 +358,61 @@ never receives these traces.
357
358
  `yagni login` again.
358
359
  - **Talking to staging** — `yagni use staging --base-url https://<staging-host>`,
359
360
  then `yagni login`.
361
+ - **`TLS certificate verification failed`, or everything fails while your
362
+ browser works** — see below.
363
+
364
+ ### Behind a corporate TLS proxy
365
+
366
+ Zero-trust agents (**Cloudflare WARP**, **Zscaler**, **Netskope**, **GlobalProtect**)
367
+ inspect HTTPS: they terminate the connection and re-sign it with a private root
368
+ CA, which the agent installs into your **operating system's** certificate store.
369
+
370
+ Your browser, `curl`, `git` and `gh` all read that store, so they keep working.
371
+ Node ships its own bundled CA list and **ignores the OS store unless asked**, so a
372
+ Node CLI is often the only thing on the machine that breaks. You do not need to
373
+ disconnect from the proxy to fix it.
374
+
375
+ When a `yagni` command fails this way it says so — the real certificate error,
376
+ not `fetch failed` — and retries itself once against your OS store. For your
377
+ sessions, tell Node to trust that store:
378
+
379
+ ```bash
380
+ export NODE_USE_SYSTEM_CA=1 # macOS, Linux
381
+ ```
382
+
383
+ ```powershell
384
+ setx NODE_USE_SYSTEM_CA 1 # Windows — then open a new terminal
385
+ ```
386
+
387
+ That trusts your OS store *in addition to* Node's bundled roots, so nothing else
388
+ changes. (`YAGNI_SYSTEM_CA=1` does the same thing scoped to YAGNI Code sessions
389
+ only.) `yagni doctor` prints a **ca trust** row showing which certificate
390
+ sources are actually in effect.
391
+
392
+ **If that variable appears to do nothing, check `node -v` first.** Node added
393
+ the two mechanisms in different releases:
394
+
395
+ | Mechanism | Works from |
396
+ | --- | --- |
397
+ | `NODE_OPTIONS=--use-system-ca` | v23.8.0, and v22.19.0 |
398
+ | `NODE_USE_SYSTEM_CA=1` | v22.19.0 and **v24.6.0** |
399
+
400
+ So on Node **24.0–24.5** the variable is silently ignored while the flag works.
401
+ Use `export NODE_OPTIONS=--use-system-ca` (or `setx NODE_OPTIONS --use-system-ca`)
402
+ on those versions — `yagni doctor` says so explicitly when it spots this.
403
+
404
+ Two more things worth knowing:
405
+
406
+ - The PEM-file alternative is `NODE_EXTRA_CA_CERTS` — **plural, with an `S`**.
407
+ The singular is a no-op, and Node ignores an unreadable path with only a
408
+ warning, so a typo looks like it worked. `yagni doctor` checks the file loads.
409
+ - On Linux below Node 24, `--use-system-ca` can't read `/etc/ssl/certs`; use
410
+ `export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt` instead.
411
+
412
+ `YAGNI_SYSTEM_CA=0` (or `NODE_USE_SYSTEM_CA=0`) switches off the automatic retry
413
+ too, if you want Node's bundled roots and nothing else. Setting
414
+ `NODE_EXTRA_CA_CERTS` does **not** opt out of anything — an extra bundle and the
415
+ OS store are additive, and you usually want both.
360
416
 
361
417
  ---
362
418
 
package/dist/cli.d.ts CHANGED
@@ -166,6 +166,45 @@ export declare function main(argv: string[]): Promise<number>;
166
166
  * test import (argv[1] points at the test runner) does not.
167
167
  */
168
168
  export declare function isEntrypoint(argv1: string | undefined, moduleUrl: string): boolean;
169
+ /** Whether the retry actually ran, or could not be started at all. */
170
+ type RetryOutcome = {
171
+ kind: "ran";
172
+ code: number;
173
+ } | {
174
+ kind: "unstarted";
175
+ reason: string;
176
+ };
177
+ /**
178
+ * The launcher's terminal error path, with one self-healing case.
179
+ *
180
+ * `fetch` reports a TLS trust failure as the bare string "fetch failed" and
181
+ * buries the OpenSSL verify code in `cause`, so the default printer told a user
182
+ * behind Cloudflare WARP or Zscaler exactly nothing (YAG-604). Here we print the
183
+ * whole cause chain, and — because Node can read the OS trust store where those
184
+ * agents install their root, and the user's machine already trusts it — retry
185
+ * the command once against that store before giving up.
186
+ *
187
+ * The original error is printed BEFORE any retry: the retry inherits stdio, so
188
+ * a child that fails differently would otherwise bury the cert failure that
189
+ * started all this, and a retry that never starts would print nothing at all.
190
+ * Note the retry re-runs the whole command, so a command with local side
191
+ * effects ahead of its first network call repeats them; every command that can
192
+ * fail this way (`login`, `doctor`, a session launch) reaches the network
193
+ * before it writes anything, and re-running them is idempotent.
194
+ */
195
+ export declare function handleFatalError(err: unknown, argv: string[], deps?: {
196
+ env?: NodeJS.ProcessEnv;
197
+ stderr?: (msg: string) => void;
198
+ platform?: NodeJS.Platform;
199
+ nodeVersion?: string;
200
+ /** The launcher's own exec flags; a retry already carrying the CA flag is not retried again. */
201
+ execArgv?: readonly string[];
202
+ /** This Node's accepted flags (defaults to process.allowedNodeEnvironmentFlags); tests pin it. */
203
+ allowedFlags?: {
204
+ has(flag: string): boolean;
205
+ };
206
+ reexec?: (argv: string[], env: NodeJS.ProcessEnv) => Promise<RetryOutcome>;
207
+ }): Promise<number>;
169
208
  /**
170
209
  * Run the launcher as the process entrypoint. Called by the published bin
171
210
  * shim (`bin.js`, after the Node version gate) and by the guard below when
package/dist/cli.js CHANGED
@@ -37,6 +37,7 @@ import { installProcessCrashHandlers } from "./crashReport.js";
37
37
  import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
38
38
  import { maybeRefreshAtLaunch } from "./refresh.js";
39
39
  import { exitCodeFor, installSignalForwarding } from "./signalForward.js";
40
+ import { altNameRemediation, describeError, findTlsCertError, isTrustStoreFixable, retryCaEnv, retryExecArgv, shouldRetryWithSystemCa, systemCaMechanism, tlsRemediation, } from "./tlsTrust.js";
40
41
  import { PAD_X } from "./padding.js";
41
42
  import { canPromptWorktreeCleanup, parseWorktreeFlag, validateWorktreeLaunchArgs, } from "./worktreeArgs.js";
42
43
  import { promptKeepOrRemoveWorktree } from "./worktreeExitPrompt.js";
@@ -889,6 +890,74 @@ export function isEntrypoint(argv1, moduleUrl) {
889
890
  };
890
891
  return resolve(argv1) === resolve(fileURLToPath(moduleUrl));
891
892
  }
893
+ /** Re-run this exact command in a child that trusts the OS certificate store. */
894
+ function reexecWithSystemCa(argv, env) {
895
+ const entry = process.argv[1];
896
+ // No resolvable entry script means nothing to re-exec (an embedded import,
897
+ // not a real CLI invocation).
898
+ if (entry === undefined) {
899
+ return Promise.resolve({ kind: "unstarted", reason: "no entry script to re-run" });
900
+ }
901
+ return new Promise((resolve) => {
902
+ // Carry the parent's exec flags (loaders, --enable-source-maps) so the
903
+ // retry runs under the same runtime as the command the user typed, plus
904
+ // --use-system-ca when this binary accepts it: on the command line of the
905
+ // one process we spawn from process.execPath it is safe, where the same
906
+ // flag in NODE_OPTIONS would reach every Node descendant, some of which
907
+ // may run under an older Node that refuses to start on it.
908
+ const child = spawn(process.execPath, [...retryExecArgv(process.execArgv), entry, ...argv], {
909
+ stdio: "inherit",
910
+ env,
911
+ });
912
+ child.on("error", (err) => resolve({ kind: "unstarted", reason: err.message }));
913
+ child.on("exit", (code, signal) => resolve({ kind: "ran", code: exitCodeFor(code, signal) }));
914
+ });
915
+ }
916
+ /**
917
+ * The launcher's terminal error path, with one self-healing case.
918
+ *
919
+ * `fetch` reports a TLS trust failure as the bare string "fetch failed" and
920
+ * buries the OpenSSL verify code in `cause`, so the default printer told a user
921
+ * behind Cloudflare WARP or Zscaler exactly nothing (YAG-604). Here we print the
922
+ * whole cause chain, and — because Node can read the OS trust store where those
923
+ * agents install their root, and the user's machine already trusts it — retry
924
+ * the command once against that store before giving up.
925
+ *
926
+ * The original error is printed BEFORE any retry: the retry inherits stdio, so
927
+ * a child that fails differently would otherwise bury the cert failure that
928
+ * started all this, and a retry that never starts would print nothing at all.
929
+ * Note the retry re-runs the whole command, so a command with local side
930
+ * effects ahead of its first network call repeats them; every command that can
931
+ * fail this way (`login`, `doctor`, a session launch) reaches the network
932
+ * before it writes anything, and re-running them is idempotent.
933
+ */
934
+ export async function handleFatalError(err, argv, deps = {}) {
935
+ const env = deps.env ?? process.env;
936
+ const write = deps.stderr ?? ((m) => void process.stderr.write(m));
937
+ const platform = deps.platform ?? process.platform;
938
+ const nodeVersion = deps.nodeVersion ?? process.versions.node;
939
+ const execArgv = deps.execArgv ?? process.execArgv;
940
+ const allowedFlags = deps.allowedFlags;
941
+ const tls = findTlsCertError(err);
942
+ write(`${describeError(err)}\n`);
943
+ if (tls && shouldRetryWithSystemCa(err, env, execArgv, nodeVersion, allowedFlags)) {
944
+ const retryEnv = retryCaEnv(env);
945
+ write(`[yagni] Retrying with your system certificate store ` +
946
+ `(${systemCaMechanism(retryExecArgv(execArgv, allowedFlags))})…\n`);
947
+ const outcome = await (deps.reexec ?? reexecWithSystemCa)(argv, retryEnv);
948
+ // A retry that RAN owns the outcome, including its own remediation block.
949
+ if (outcome.kind === "ran")
950
+ return outcome.code;
951
+ write(`[yagni] Could not start the retry: ${outcome.reason}\n`);
952
+ }
953
+ if (tls) {
954
+ const lines = isTrustStoreFixable(tls.code)
955
+ ? tlsRemediation(platform, nodeVersion, allowedFlags)
956
+ : altNameRemediation();
957
+ write(`${lines.join("\n")}\n`);
958
+ }
959
+ return 1;
960
+ }
892
961
  /**
893
962
  * Run the launcher as the process entrypoint. Called by the published bin
894
963
  * shim (`bin.js`, after the Node version gate) and by the guard below when
@@ -902,10 +971,7 @@ export function runAsEntrypoint() {
902
971
  installProcessCrashHandlers({ client: "cli", clientVersion: cliVersion() });
903
972
  main(process.argv.slice(2))
904
973
  .then((code) => process.exit(code))
905
- .catch((err) => {
906
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
907
- process.exit(1);
908
- });
974
+ .catch((err) => handleFatalError(err, process.argv.slice(2)).then((code) => process.exit(code), () => process.exit(1)));
909
975
  }
910
976
  // Only auto-run when invoked as the CLI entry, so tests can import this module
911
977
  // (e.g. to exercise wantsHelp) without spawning the agent.
package/dist/doctor.d.ts CHANGED
@@ -47,9 +47,36 @@ export type BackendProbe = {
47
47
  } | {
48
48
  kind: "status";
49
49
  status: number;
50
+ }
51
+ /** The chain didn't validate: a TLS-inspecting proxy, not a dead network. */
52
+ | {
53
+ kind: "tls";
54
+ code: string;
50
55
  } | {
51
56
  kind: "network";
52
57
  };
58
+ /** What the running process has been told about certificate authorities. */
59
+ export interface CaTrustProbe {
60
+ /** `NODE_USE_SYSTEM_CA` is on. */
61
+ systemCa: boolean;
62
+ /** `NODE_OPTIONS` carries `--use-system-ca` — the version-proof form. */
63
+ systemCaFlag: boolean;
64
+ /** Whether this Node honours the variable at all (v22.19+ / v24.6+). */
65
+ systemCaEnvHonoured: boolean;
66
+ /** The running Node version, for the "your Node ignores it" message. */
67
+ nodeVersion: string;
68
+ /**
69
+ * Whether buildLaunch will ADD the OS store to a session that this process
70
+ * did not itself inherit (the YAGNI_SYSTEM_CA=1 opt-in). Strictly "will add":
71
+ * a user who exported NODE_USE_SYSTEM_CA themselves is reported through
72
+ * `systemCa` above, not here — the variable passes into the session as-is.
73
+ */
74
+ launchAddsSystemCa: boolean;
75
+ /** The `NODE_EXTRA_CA_CERTS` path, when set. */
76
+ extraCertsPath?: string;
77
+ /** Whether that path is actually readable — Node only warns when it isn't. */
78
+ extraCertsReadable?: boolean;
79
+ }
53
80
  /**
54
81
  * The Node floor, first in the list because every other check is moot
55
82
  * without it: an old Node dies inside pi's HTTP client mid-request (Sentry
@@ -63,6 +90,19 @@ export declare function checkProfileToken(profile: Pick<Profile, "name" | "token
63
90
  export declare function checkTokenExpiry(status: TokenExpiryStatus): CheckResult;
64
91
  export declare function checkBackend(probe: BackendProbe): CheckResult;
65
92
  export declare function checkStateDir(probe: StateDirProbe): CheckResult;
93
+ /**
94
+ * What this machine trusts, and whether the user's attempt to widen it took.
95
+ *
96
+ * Advisory by design: the default (Node's bundled roots, neither variable set)
97
+ * is correct on an unmanaged machine, so it is never a failure on its own — the
98
+ * backend check is what goes red when interception actually breaks things. The
99
+ * row exists because both failure modes here are invisible otherwise: Node
100
+ * ignores an unreadable `NODE_EXTRA_CA_CERTS` with a warning most users never
101
+ * see, and the variable is routinely mistyped in the singular (which is exactly
102
+ * how YAG-604 stayed unsolved — `NODE_EXTRA_CA_CERT` looks right and does
103
+ * nothing).
104
+ */
105
+ export declare function checkCaTrust(probe: CaTrustProbe): CheckResult;
66
106
  export declare function checkCliUpdate(probe: {
67
107
  current: string;
68
108
  latest: string | null;
@@ -138,12 +178,17 @@ export interface DoctorDeps {
138
178
  probeLatestVersion?: () => Promise<string | null>;
139
179
  /** MCP config snapshot (config-only, no connections). */
140
180
  probeMcp?: () => Promise<McpProbe>;
181
+ /** Certificate-authority configuration of the running process. */
182
+ probeCaTrust?: () => CaTrustProbe;
141
183
  /** OTel gate resolution (env / workspace / repo settings); tests stub it. */
142
184
  resolveOtel?: (profile: Profile) => Promise<OtelLaunchConfig | undefined>;
143
185
  /** OTel export probe: one real record per signal against the session env. */
144
186
  probeOtel?: (env: NodeJS.ProcessEnv) => Promise<OtelProbeSignal[]>;
145
187
  log?: (msg: string) => void;
146
188
  }
189
+ export declare function defaultProbeBackend(baseUrl: string, token: string): Promise<BackendProbe>;
190
+ /** Read the process's CA configuration, checking the extra bundle really loads. */
191
+ export declare function defaultProbeCaTrust(env?: NodeJS.ProcessEnv, nodeVersion?: string): CaTrustProbe;
147
192
  /** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
148
193
  export declare function ghOnPathDefault(env?: NodeJS.ProcessEnv): boolean;
149
194
  /**
package/dist/doctor.js CHANGED
@@ -22,6 +22,7 @@ import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir, resolveTel
22
22
  import { readActiveProfile } from "./profiles.js";
23
23
  import { resolveMcpConfigPath } from "./mcpCommand.js";
24
24
  import { MIN_NODE_VERSION, nodeVersionSatisfies } from "./nodeVersion.js";
25
+ import { EXTRA_CA_ENV, SYSTEM_CA_ENV, SYSTEM_CA_FLAG, childCaEnv, findTlsCertError, nodeOptionsHaveSystemCa, systemCaEnabled, systemCaEnvSupported, } from "./tlsTrust.js";
25
26
  // ── Pure check builders ─────────────────────────────────────────────────────
26
27
  /**
27
28
  * The Node floor, first in the list because every other check is moot
@@ -123,6 +124,19 @@ export function checkBackend(probe) {
123
124
  required: false,
124
125
  };
125
126
  }
127
+ if (probe.kind === "tls") {
128
+ return {
129
+ name: "backend",
130
+ status: "fail",
131
+ detail: `TLS certificate verification failed (${probe.code})`,
132
+ hint: `a TLS-inspecting proxy (Cloudflare WARP, Zscaler, Netskope) is re-signing HTTPS — ` +
133
+ (systemCaEnvSupported()
134
+ ? `set ${SYSTEM_CA_ENV}=1 to trust your OS certificate store`
135
+ : `set NODE_OPTIONS=${SYSTEM_CA_FLAG} to trust your OS certificate store ` +
136
+ `(this Node ignores ${SYSTEM_CA_ENV})`),
137
+ required: true,
138
+ };
139
+ }
126
140
  if (probe.kind === "network") {
127
141
  return {
128
142
  name: "backend",
@@ -183,6 +197,66 @@ export function checkStateDir(probe) {
183
197
  }
184
198
  return { name: "state dir", status: "ok", detail: `${probe.path} is 0700`, required: false };
185
199
  }
200
+ /**
201
+ * What this machine trusts, and whether the user's attempt to widen it took.
202
+ *
203
+ * Advisory by design: the default (Node's bundled roots, neither variable set)
204
+ * is correct on an unmanaged machine, so it is never a failure on its own — the
205
+ * backend check is what goes red when interception actually breaks things. The
206
+ * row exists because both failure modes here are invisible otherwise: Node
207
+ * ignores an unreadable `NODE_EXTRA_CA_CERTS` with a warning most users never
208
+ * see, and the variable is routinely mistyped in the singular (which is exactly
209
+ * how YAG-604 stayed unsolved — `NODE_EXTRA_CA_CERT` looks right and does
210
+ * nothing).
211
+ */
212
+ export function checkCaTrust(probe) {
213
+ // The trap that makes a correct-looking fix do nothing: the variable landed
214
+ // in v22.19 and v24.6, the flag back in v23.8, so a 24.0-24.5 user who
215
+ // exported NODE_USE_SYSTEM_CA gets silence and concludes it didn't help.
216
+ if (probe.systemCa && !probe.systemCaEnvHonoured && !probe.systemCaFlag) {
217
+ return {
218
+ name: "ca trust",
219
+ status: "warn",
220
+ detail: `${SYSTEM_CA_ENV} is set, but Node v${probe.nodeVersion} ignores it`,
221
+ hint: `that variable needs Node v24.6.0+ or v22.19.0+ — use NODE_OPTIONS=${SYSTEM_CA_FLAG} instead, which this Node accepts`,
222
+ required: false,
223
+ };
224
+ }
225
+ if (probe.extraCertsPath !== undefined && probe.extraCertsReadable === false) {
226
+ return {
227
+ name: "ca trust",
228
+ status: "warn",
229
+ detail: `${EXTRA_CA_ENV} points at ${probe.extraCertsPath}, which can't be read`,
230
+ hint: `Node ignores an unreadable bundle with only a warning — fix the path, or use ${SYSTEM_CA_ENV}=1 instead`,
231
+ required: false,
232
+ };
233
+ }
234
+ const sources = [];
235
+ if (probe.systemCaFlag || (probe.systemCa && probe.systemCaEnvHonoured)) {
236
+ sources.push("OS certificate store");
237
+ }
238
+ if (probe.extraCertsPath !== undefined)
239
+ sources.push(probe.extraCertsPath);
240
+ if (sources.length === 0) {
241
+ // Report what a SESSION gets, not only what doctor itself inherited: with
242
+ // the opt-in, buildLaunch adds the OS store to the child env, and a bare
243
+ // "Node's bundled CAs" here would understate the live configuration.
244
+ return {
245
+ name: "ca trust",
246
+ status: "ok",
247
+ detail: probe.launchAddsSystemCa
248
+ ? "this process: Node's bundled CAs; sessions also trust the OS store (YAGNI_SYSTEM_CA=1)"
249
+ : "Node's bundled CAs (no OS store, no extra bundle)",
250
+ required: false,
251
+ };
252
+ }
253
+ return {
254
+ name: "ca trust",
255
+ status: "ok",
256
+ detail: `bundled CAs + ${sources.join(" + ")}`,
257
+ required: false,
258
+ };
259
+ }
186
260
  export function checkCliUpdate(probe) {
187
261
  if (probe.latest === null) {
188
262
  return {
@@ -460,7 +534,7 @@ async function defaultProbeMcp(env = process.env) {
460
534
  };
461
535
  }
462
536
  }
463
- async function defaultProbeBackend(baseUrl, token) {
537
+ export async function defaultProbeBackend(baseUrl, token) {
464
538
  if (!token)
465
539
  return { kind: "skipped" };
466
540
  try {
@@ -471,9 +545,38 @@ async function defaultProbeBackend(baseUrl, token) {
471
545
  });
472
546
  return { kind: "status", status: res.status };
473
547
  }
548
+ catch (err) {
549
+ // `fetch` collapses a certificate failure into "fetch failed" and hides the
550
+ // verify code in `cause`; separating the two is the whole point of the row.
551
+ const tls = findTlsCertError(err);
552
+ return tls ? { kind: "tls", code: tls.code } : { kind: "network" };
553
+ }
554
+ }
555
+ /** Read the process's CA configuration, checking the extra bundle really loads. */
556
+ export function defaultProbeCaTrust(env = process.env, nodeVersion = process.versions.node) {
557
+ const base = {
558
+ systemCa: systemCaEnabled(env),
559
+ systemCaFlag: nodeOptionsHaveSystemCa(env),
560
+ systemCaEnvHonoured: systemCaEnvSupported(nodeVersion),
561
+ nodeVersion,
562
+ // Exactly what buildLaunch will decide for the session, asked the same way.
563
+ launchAddsSystemCa: Object.keys(childCaEnv(env)).length > 0,
564
+ };
565
+ const extraCertsPath = env[EXTRA_CA_ENV]?.trim();
566
+ if (!extraCertsPath)
567
+ return base;
568
+ let extraCertsReadable = false;
569
+ try {
570
+ // A regular file only: readFileSync on a FIFO or device would block doctor
571
+ // forever, and Node itself would not have loaded such a path either.
572
+ extraCertsReadable =
573
+ statSync(extraCertsPath).isFile() &&
574
+ readFileSync(extraCertsPath, "utf8").includes("BEGIN CERTIFICATE");
575
+ }
474
576
  catch {
475
- return { kind: "network" };
577
+ extraCertsReadable = false;
476
578
  }
579
+ return { ...base, extraCertsPath, extraCertsReadable };
477
580
  }
478
581
  /** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
479
582
  export function ghOnPathDefault(env = process.env) {
@@ -527,6 +630,7 @@ export async function gatherChecks(deps = {}) {
527
630
  const probeBackend = deps.probeBackend ?? defaultProbeBackend;
528
631
  const probeStateDir = deps.probeStateDir ?? defaultProbeStateDir;
529
632
  const probeMcp = deps.probeMcp ?? (() => defaultProbeMcp());
633
+ const probeCaTrust = deps.probeCaTrust ?? (() => defaultProbeCaTrust());
530
634
  const ghOnPath = deps.ghOnPath ?? (() => ghOnPathDefault());
531
635
  const platform = deps.platform ?? process.platform;
532
636
  const probeBash = deps.probeBash ?? (() => bashOnWindowsDefault());
@@ -550,6 +654,7 @@ export async function gatherChecks(deps = {}) {
550
654
  if (profile.token) {
551
655
  checks.push(checkTokenExpiry(classifyTokenExpiry(profile.expiresAt, now())));
552
656
  }
657
+ checks.push(checkCaTrust(probeCaTrust()));
553
658
  const backend = profile.token
554
659
  ? await probeBackend(profile.baseUrl, profile.token)
555
660
  : { kind: "skipped" };
@@ -131,7 +131,7 @@ export function makeAskAdvisorTool(opts) {
131
131
  "Call ask_advisor only for a genuine judgment fork — an architectural choice, a subtle correctness question, or a second opinion before an approach you would have to unwind. Reading the code is cheaper; do that first.",
132
132
  "Ask ONE specific question per consult, and include the excerpts that matter. The advisor reads the repo itself, so point it at the right place rather than pasting everything.",
133
133
  "Consults are capped per session. Spend them on the calls you would otherwise get wrong.",
134
- "The advice comes back as plain text: act on it, and call record_decision when it settles a product-intent call so the next agent inherits it.",
134
+ "The advice comes back as plain text: act on it, and call record_decision when it settles a consequential product-intent or architecture call so the next agent inherits it.",
135
135
  ]
136
136
  : [
137
137
  "Call ask_advisor only for a genuine judgment fork — an architectural choice, a subtle correctness question, or a second opinion before an approach you would have to unwind. Reading the code is cheaper; do that first.",
@@ -54,7 +54,7 @@ export function makeAskYagniTool(opts) {
54
54
  "Call ask_yagni BEFORE guessing about anything organization- or codebase-specific (conventions, policies, architecture, ownership, product decisions).",
55
55
  "Pass the user's actual question; add relevant local context (file paths, snippets) in the optional `context` field.",
56
56
  "When you use an answer, quote or reference its citations so the user can verify the source.",
57
- "Answers carry a standing: treat a confirmed decision as settled; when you lean on an unverified assumption or an inference, say so where the work is reviewed; when there is no recorded position, follow the answer's instruction to record the assumption you proceed on.",
57
+ "Answers carry a standing: treat a confirmed decision as settled; when you lean on an unverified assumption or an inference, say so where the work is reviewed; when there is no recorded position, follow the answer's instruction: record the assumption you proceed on only when it is a consequential call another engineer would need to know.",
58
58
  ],
59
59
  parameters,
60
60
  // Self-framed: the condensed transcript look has no tinted tool boxes.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Commit and pull-request attribution settings.
3
+ *
4
+ * Mirrors Claude Code's `attribution` settings object so an engineering org
5
+ * that already labels AI-assisted commits keeps one convention across
6
+ * harnesses:
7
+ *
8
+ * { "attribution": { "commit": "<trailer text>", "pr": "<PR body line>" } }
9
+ *
10
+ * Tiers, later wins per FIELD (a tier may set only `commit` or only `pr`):
11
+ * 1. Claude Code settings, as a compatibility fallback so an org configured
12
+ * for Claude Code needs no second file: ~/.claude/settings.json,
13
+ * .claude/settings.json, .claude/settings.local.json. Both the
14
+ * `attribution` object and the deprecated `includeCoAuthoredBy: false`
15
+ * (which hides both pieces) are honored. Off under the Claude-compat
16
+ * kill switch.
17
+ * 2. YAGNI Code settings: ~/.yagni-code/config.json (user),
18
+ * .yagni-code/config.json (project, committed so an org sets it once),
19
+ * .yagni-code/config.local.json (local).
20
+ *
21
+ * An empty string hides that piece; an absent field keeps the default. The
22
+ * defaults are the strings automation already keys on — the commit trailer
23
+ * matches the /go pipeline's provenance block, and the PR line is what an
24
+ * auto-labeller matches — so they are treated as stable.
25
+ *
26
+ * Workspace trust: the project and local tiers (YAGNI's and Claude Code's)
27
+ * are repo-controlled files, and the text they carry becomes a standing
28
+ * system-prompt directive, so they apply only once the folder is trusted —
29
+ * the same gate as project hooks and project allow-rules (a hostile repo
30
+ * can COMMIT a config.local.json, so local is gated exactly like project).
31
+ * Untrusted, the user tiers and the defaults stand, and a skipped tier that
32
+ * actually carried attribution fields leaves a warning so the developer can
33
+ * see why their repo's trailer did not apply.
34
+ *
35
+ * Fail-soft, same posture as the hooks and permission-rule loaders: a
36
+ * malformed file is skipped whole with a warn (path + error class only,
37
+ * never file content), and a non-string field is ignored.
38
+ */
39
+ export interface AttributionSettings {
40
+ /** The trailer appended to commit messages; "" hides it. */
41
+ commit: string;
42
+ /** The line appended to pull request descriptions; "" hides it. */
43
+ pr: string;
44
+ }
45
+ export declare const DEFAULT_COMMIT_ATTRIBUTION = "Co-Authored-By: YAGNI Code <code@yagni.app>";
46
+ export declare const DEFAULT_PR_ATTRIBUTION = "Generated with [YAGNI Code](https://yagni.app/code)";
47
+ export declare const DEFAULT_ATTRIBUTION: AttributionSettings;
48
+ export type AttributionSource = "claude-user" | "claude-project" | "claude-local" | "user" | "project" | "local";
49
+ export interface AttributionLoad {
50
+ settings: AttributionSettings;
51
+ /**
52
+ * Which tier last set each piece; absent when the default stands. Rides
53
+ * the boot trail's `attribution_configured` line so support can tell a
54
+ * repo-set trailer from a personal one without reading the files.
55
+ */
56
+ sources: {
57
+ commit?: AttributionSource;
58
+ pr?: AttributionSource;
59
+ };
60
+ /** Repo tiers that carried attribution but were skipped because the folder is untrusted. */
61
+ skipped: AttributionSource[];
62
+ warnings: string[];
63
+ }
64
+ export interface LoadAttributionOptions {
65
+ /**
66
+ * Workspace trust (pi's project trust model). `false` skips the project
67
+ * and local tiers (YAGNI's and Claude Code's); absent reads as trusted,
68
+ * matching the sandbox's trust probe default.
69
+ */
70
+ trusted?: boolean;
71
+ }
72
+ /**
73
+ * Normalize one configured piece: trim trailing whitespace per line, strip
74
+ * control characters (a trailer rides a git commit and the system prompt),
75
+ * and clamp to a few short lines. "" stays "" (the hide signal).
76
+ */
77
+ export declare function normalizeAttributionText(value: string): string;
78
+ /**
79
+ * Resolve the attribution settings for a session. Read once at boot (like the
80
+ * grounding switch); a change lands on the next session start.
81
+ */
82
+ export declare function loadAttributionSettings(userHome?: string, cwd?: string, env?: NodeJS.ProcessEnv, options?: LoadAttributionOptions): AttributionLoad;
83
+ export declare const ATTRIBUTION_HEADER = "## Commit and pull request attribution";
84
+ /**
85
+ * The system-prompt section that tells the model what to append. Pure. Null
86
+ * when both pieces are hidden, so the prompt carries no attribution talk at
87
+ * all — an org that opted out gets a prompt byte-identical to one that never
88
+ * had the feature.
89
+ */
90
+ export declare function attributionPromptSection(settings: AttributionSettings): string | null;
91
+ //# sourceMappingURL=attribution.d.ts.map