@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 +56 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +70 -4
- package/dist/doctor.d.ts +45 -0
- package/dist/doctor.js +107 -2
- package/dist/extension/askAdvisorTool.js +1 -1
- package/dist/extension/askYagniTool.js +1 -1
- package/dist/extension/attribution.d.ts +91 -0
- package/dist/extension/attribution.js +187 -0
- package/dist/extension/branding.d.ts +6 -0
- package/dist/extension/branding.js +9 -0
- package/dist/extension/config.d.ts +7 -0
- package/dist/extension/config.js +1 -0
- package/dist/extension/decisionRubric.d.ts +20 -0
- package/dist/extension/decisionRubric.js +26 -0
- package/dist/extension/decisions.d.ts +27 -5
- package/dist/extension/decisions.js +74 -10
- package/dist/extension/index.d.ts +7 -0
- package/dist/extension/index.js +121 -13
- package/dist/extension/pipeline/finish.d.ts +14 -3
- package/dist/extension/pipeline/finish.js +13 -5
- package/dist/extension/pipeline/goCommand.d.ts +10 -0
- package/dist/extension/pipeline/goCommand.js +6 -0
- package/dist/extension/pipeline/personas.d.ts +1 -1
- package/dist/extension/pipeline/personas.js +4 -4
- package/dist/extension/pipeline/stages.js +4 -2
- package/dist/extension/recall.d.ts +2 -0
- package/dist/extension/recall.js +11 -1
- package/dist/extension/recordDecisionTool.d.ts +11 -1
- package/dist/extension/recordDecisionTool.js +24 -11
- package/dist/launch.js +9 -0
- package/dist/tlsTrust.d.ts +188 -0
- package/dist/tlsTrust.js +375 -0
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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
|