@yagni-app/code-staging 1.2.6-staging.2011.1 → 1.2.6-staging.2020.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.
@@ -173,8 +173,8 @@ export { recordDecision } from "./recordDecisionTool.js";
173
173
  export type { RecordDecisionParams } from "./recordDecisionTool.js";
174
174
  export { runInitPass, runTeamSetup, registerTeamSetupCommand, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
175
175
  export type { RunInitPassDeps, InitPassOutcome, RunTeamSetupDeps, TeamSetupOutcome, RepoIntake, RepoIntakeFs, EngineeringDraft, DraftTeam, } from "./initPass.js";
176
- export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
177
- export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA, BRAND_NAME } from "./branding.js";
176
+ export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest, } from "./initDone.js";
177
+ export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA, BRAND_NAME, } from "./branding.js";
178
178
  export { loadHooksConfig, makeHookRunner, registerHooks, matchesMatcher, parsePreToolUseOutput, parsePermissionRequestOutput, parseAdditionalContext, parseCompactCancel, HOOK_SUPPORTED_EVENTS, } from "./hooks.js";
179
179
  export type { HookEntry, HookGroup, HooksConfig, HookEventName, HookRunner, PreToolUseHookResult, PermissionRequestHookResult, RegisterHooksDeps, } from "./hooks.js";
180
180
  export { createUltraHolder, registerUltraCommand } from "./ultra.js";
@@ -188,16 +188,16 @@ export { runPipeline } from "./pipeline/orchestrator.js";
188
188
  export type { RunPipelineDeps } from "./pipeline/orchestrator.js";
189
189
  export { HEADLESS_GO_EXIT, HEADLESS_GO_USAGE, parseHeadlessGoArgs, runHeadlessGo, validateHeadlessGoArgs, } from "./pipeline/headlessGo.js";
190
190
  export type { HeadlessGoArgs, HeadlessGoDeps, HeadlessGoOutcome, HeadlessGoResult, HeadlessStageRow, } from "./pipeline/headlessGo.js";
191
- export { isMissionMode, missionSeed, missionSkippedStages, normalizeMission } from "./pipeline/mission.js";
191
+ export { isMissionMode, missionSeed, missionSkippedStages, normalizeMission, } from "./pipeline/mission.js";
192
192
  export type { MissionInputs } from "./pipeline/mission.js";
193
193
  export { clampTier, parseTierCap, resolveTierCap, TIER_CAP_ENV } from "./pipeline/tierCap.js";
194
194
  export { withResilience, classifyTransient, composeAbortSignal } from "./pipeline/resilience.js";
195
- export type { ResilienceAttemptRecord, ResilienceSeams, RunStageFn } from "./pipeline/resilience.js";
195
+ export type { ResilienceAttemptRecord, ResilienceSeams, RunStageFn, } from "./pipeline/resilience.js";
196
196
  export { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
197
197
  export type { ResiliencePolicy } from "./pipeline/types.js";
198
- export { buildVerifyEnv, detectVerifyCommand, makeRunVerify, parseVerifyFailures } from "./pipeline/verify.js";
198
+ export { buildVerifyEnv, detectVerifyCommand, makeRunVerify, parseVerifyFailures, } from "./pipeline/verify.js";
199
199
  export type { VerifyCommand, VerifyOutcome } from "./pipeline/verify.js";
200
- export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport } from "./pipeline/eval.js";
200
+ export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport, } from "./pipeline/eval.js";
201
201
  export type { ComparisonReport, LaneFit, LaneOutcome } from "./pipeline/eval.js";
202
202
  export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
203
203
  export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
@@ -211,9 +211,9 @@ export type { PermissionMode, PermissionPolicy, GateDecision, RegisterPermission
211
211
  export { makeBlessStore, blessPath } from "./bless.js";
212
212
  export type { BlessStore, BlessRule } from "./bless.js";
213
213
  export { bankDecision, fetchDecisions, supersedeDecision, parseDecideArgs, formatDecisionsList, shortId, registerDecisionCommands, DEFAULT_LIST_LIMIT, } from "./decisions.js";
214
- export type { DecisionClientOpts, DecisionListItem, BankDecisionParams, DecideArgs } from "./decisions.js";
214
+ export type { DecisionClientOpts, DecisionListItem, BankDecisionParams, DecideArgs, } from "./decisions.js";
215
215
  export { makeDecisionCapture, CAPTURE_DEBOUNCE_MS } from "./decisionCapture.js";
216
- export type { DecisionCapture, DecisionCaptureDeps, BlessCaptureInfo, CaptureCtx } from "./decisionCapture.js";
216
+ export type { DecisionCapture, DecisionCaptureDeps, BlessCaptureInfo, CaptureCtx, } from "./decisionCapture.js";
217
217
  export { registerAmbientRecall, fetchRecall, formatRecallBlock, toRepoRelativePath, normalizeRecall, RECALL_TIMEOUT_MS, RECALL_MIN_DECISIONS, } from "./recall.js";
218
218
  export type { RecallResult, RecallClientOpts, RegisterRecallDeps } from "./recall.js";
219
219
  export { makeCostAccumulator, formatCostLine, registerCostCommand, cacheHitPercent, spendTokenTotals, } from "./costHud.js";
@@ -231,5 +231,5 @@ export type { RowKind, ToolRow, ToolRowPatch } from "./toolRuns.js";
231
231
  export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
232
232
  export type { WorkingLineHandle, RegisterWorkingLineDeps } from "./workingLine.js";
233
233
  export { crashReportsDisabled, installNonZeroExitReporter, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
234
- export type { CrashReporter, CrashReporterOpts, FatalCrashOpts, SanitizedCrash } from "./crashReport.js";
234
+ export type { CrashReporter, CrashReporterOpts, FatalCrashOpts, SanitizedCrash, } from "./crashReport.js";
235
235
  //# sourceMappingURL=index.d.ts.map
@@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import { DEFAULT_ADVISOR_LIMITS, formatAdvisorSubtotal, makeAdvisorState } from "./advisor.js";
4
4
  import { makeChildUsageState } from "./childUsage.js";
5
- import { appendGrant, createGrantsRefresher, loadGrants, resolveRepoKey, storagePrefix } from "./permission/approvedPrefixes.js";
5
+ import { appendGrant, createGrantsRefresher, loadGrants, resolveRepoKey, storagePrefix, } from "./permission/approvedPrefixes.js";
6
6
  import { redactCommand } from "./redact.js";
7
- import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs } from "./permission/guardian.js";
7
+ import { formatGuardianSubtotal, GUARDIAN_MODEL_TIER, makeGuardianState, resolveGuardianLimits, reviewCommand, deriveGuardianTimeoutMs, } from "./permission/guardian.js";
8
8
  import { makeAskAdvisorTool, registerAdviseCommand } from "./askAdvisorTool.js";
9
9
  import { makeAskUserQuestionTool } from "./askUserQuestionTool.js";
10
10
  import { makeAskYagniTool } from "./askYagniTool.js";
@@ -18,9 +18,9 @@ import { makePrototypeVerifyTool } from "./prototypeVerify.js";
18
18
  import { makeRecordDecisionTool } from "./recordDecisionTool.js";
19
19
  import { makeSuggestNextWorkTool } from "./nextWorkTool.js";
20
20
  import { makePrototypeCreateTool, makePrototypeAttachTool, makePrototypeReadTool, makePrototypeDraftTool, makePrototypeVerificationTool, makePrototypeReviseTool, makePrototypeListTool, makePrototypeShareTool, } from "./prototypeTool.js";
21
- import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_DRIVER_GROUNDING_FREE, YAGNI_IDENTITY_GROUNDING_FREE, YAGNI_IDENTITY_ULTRA, YAGNI_IDENTITY_ULTRA_GROUNDING_FREE } from "./branding.js";
21
+ import { BRAND_NAME, brandSystemPrompt, brandingDisabled, buildMastheadString, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_DRIVER_GROUNDING_FREE, YAGNI_IDENTITY_GROUNDING_FREE, YAGNI_IDENTITY_ULTRA, YAGNI_IDENTITY_ULTRA_GROUNDING_FREE, } from "./branding.js";
22
22
  import { claudeRulesSection } from "./claudeRules.js";
23
- import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadOwnerRootFor, scratchpadSection } from "./scratchpad.js";
23
+ import { ensureScratchpadDir, SCRATCHPAD_TMPDIR_ENV, scratchpadDir as scratchpadDirFor, scratchpadOwnerRootFor, scratchpadSection, } from "./scratchpad.js";
24
24
  import { registerCostCommand } from "./costHud.js";
25
25
  import { isDebug } from "./diagnostics.js";
26
26
  import { logEvent } from "./errorSink.js";
@@ -29,25 +29,26 @@ import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
29
29
  import { codeStateHome } from "./stateHome.js";
30
30
  import { mutateLocalConfig } from "./settingsFiles.js";
31
31
  import { logTurnLifecycle } from "./turnLog.js";
32
- import { createYagniFooterFactory, cyclePermissionMode, formatCwd, GIT_MUTATING_PATTERN, isShiftTab } from "./footer.js";
32
+ import { createYagniFooterFactory, cyclePermissionMode, formatCwd, GIT_MUTATING_PATTERN, isShiftTab, } from "./footer.js";
33
33
  import { RerouteNotifier } from "./rerouteNotice.js";
34
- import { isFreshWorkspace, registerTeamSetupCommand, runInitPass as defaultRunInitPass } from "./initPass.js";
35
- import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone } from "./initDone.js";
34
+ import { isFreshWorkspace, registerTeamSetupCommand, runInitPass as defaultRunInitPass, } from "./initPass.js";
35
+ import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone, } from "./initDone.js";
36
36
  import { looksMutating } from "./mcp/tools.js";
37
- import { startMcp as defaultStartMcp, wireShutdown, deriveStartupConnectivityNotices } from "./mcp/startup.js";
37
+ import { startMcp as defaultStartMcp, wireShutdown, deriveStartupConnectivityNotices, } from "./mcp/startup.js";
38
38
  import { registerGoCommand } from "./pipeline/goCommand.js";
39
39
  import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
40
- import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
40
+ import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate, } from "./permission/gate.js";
41
41
  import { setBgJobOutcomeReporter } from "./bgJobs.js";
42
42
  import { registerBgJobsUi } from "./bgJobsRender.js";
43
43
  import { registerSandbox } from "./sandbox/session.js";
44
- import { isSandboxDenialOutput, sandboxAutoAllowDecision, sandboxDenialSignature, shouldUseSandbox } from "./sandbox/bash.js";
44
+ import { isSandboxDenialOutput, sandboxAutoAllowDecision, sandboxDenialSignature, shouldUseSandbox, } from "./sandbox/bash.js";
45
45
  import { sandboxFailureKey } from "./permission/approvedPrefixes.js";
46
46
  import { createEscapeTally } from "./sandbox/escapeTally.js";
47
47
  import { createGrantCensus } from "./permission/grantCensus.js";
48
- import { runsInSandboxHarness, toolchainSpawnHookFromEnv, toolchainUserBashOperations } from "./toolchain/spawnHook.js";
48
+ import { runsInSandboxHarness, toolchainBashToolDefinition, toolchainSpawnHookFromEnv, toolchainUserBashOperations, } from "./toolchain/spawnHook.js";
49
49
  import { CLOUD_SESSION_ENV } from "./pipeline/cloud.js";
50
50
  import { registerStageClock } from "./pipeline/stageClock.js";
51
+ import { scrubSecrets } from "./pipeline/scrubSecrets.js";
51
52
  import { registerTelemetry as defaultRegisterTelemetry } from "./telemetry/register.js";
52
53
  import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
53
54
  import { loadPermissionRules } from "./permissionRules/loadConfig.js";
@@ -63,11 +64,11 @@ import { registerAmbientRecall } from "./recall.js";
63
64
  import { resilientFetch } from "./resilientFetch.js";
64
65
  import { installNonZeroExitReporter, installUncaughtExceptionMonitor, makeCrashReporter, runningUnderTest, } from "./crashReport.js";
65
66
  import { createToolOutcomeBatcher } from "./toolOutcomes.js";
66
- import { flushSpool as defaultFlushSpool } from "./spool.js";
67
+ import { flushSpool as defaultFlushSpool, } from "./spool.js";
67
68
  import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
68
69
  import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
69
70
  import { buildYagniProvider } from "./provider.js";
70
- import { orderDriverCatalog, registerStartTierGuard, registerTierCommand, trailReason } from "./tierCommand.js";
71
+ import { orderDriverCatalog, registerStartTierGuard, registerTierCommand, trailReason, } from "./tierCommand.js";
71
72
  import { makeModelPickerListener } from "./modelPicker.js";
72
73
  import { registerChipEditor } from "./chipEditor.js";
73
74
  import { registerSlashCommandFilter } from "./slashCommandFilter.js";
@@ -160,6 +161,17 @@ export async function registerYagni(pi, deps = {}) {
160
161
  const fetchCatalog = deps.fetchCatalog ?? defaultFetchCatalog;
161
162
  const env = deps.env ?? process.env;
162
163
  const evalMode = isEvalMode(env);
164
+ // A stage child of `yagni go --headless --cloud` (the one-attempt lane):
165
+ // headlessGo sets the variable where it parses the flag, and the lane's
166
+ // launch line exports the toolchain file. It runs outside eval mode, so it
167
+ // is told its sandbox and gets team_read on these two signals alone; the
168
+ // flag outside a sandbox changes nothing.
169
+ const cloudSession = runsInSandboxHarness(env) && env[CLOUD_SESSION_ENV] === "1";
170
+ // A sandbox session is told what its sandbox has instead of a laptop's
171
+ // `gh` recipes: a cloud session, or a legacy build or scoping session (eval
172
+ // mode under the harness, where team_read is registered). A local eval
173
+ // compare sets eval mode without the harness, so it keeps the laptop's.
174
+ const sandboxSession = cloudSession || (evalMode && runsInSandboxHarness(env));
163
175
  // A timed /go stage child refuses every tool once its time is up. Order does
164
176
  // not matter: pi runs every tool_call handler until one blocks, and an allow
165
177
  // (a result without `block`) never ends the loop, so this block always lands.
@@ -238,7 +250,9 @@ export async function registerYagni(pi, deps = {}) {
238
250
  logAttributionProvenance(attributionState.load);
239
251
  return;
240
252
  }
241
- attributionState.load = loadAttributionSettings(undefined, ctx.cwd || process.cwd(), env, { trusted });
253
+ attributionState.load = loadAttributionSettings(undefined, ctx.cwd || process.cwd(), env, {
254
+ trusted,
255
+ });
242
256
  logAttributionProvenance(attributionState.load);
243
257
  };
244
258
  // The refreshing TokenProvider replaces the old boot-time token snapshot:
@@ -279,7 +293,12 @@ export async function registerYagni(pi, deps = {}) {
279
293
  // like every other external side effect; injectable for tests.
280
294
  const authReporter = deps.authReporter ??
281
295
  (!evalMode
282
- ? makeCrashReporter({ baseUrl, getToken: getTokenFn, fetchImpl: deps.fetchImpl, env: deps.env })
296
+ ? makeCrashReporter({
297
+ baseUrl,
298
+ getToken: getTokenFn,
299
+ fetchImpl: deps.fetchImpl,
300
+ env: deps.env,
301
+ })
283
302
  : async () => { });
284
303
  // YAG-500 Fix A+C: the model-path 401 recovery outcome, set by the
285
304
  // message_end handler so it can produce the right user-facing message. The
@@ -372,8 +391,9 @@ export async function registerYagni(pi, deps = {}) {
372
391
  // standard-tier extraction, replacing the bash + curl + python dance.
373
392
  pi.registerTool(makeWebFetchTool(toolOpts));
374
393
  // The Team's connected tools, read through YAGNI as the Team. Only a run's
375
- // sandbox holds the run token the route requires, so only it gets the tool.
376
- if (evalMode) {
394
+ // sandbox holds the run token the route requires, so only it gets the tool:
395
+ // a legacy build or scoping session (eval mode) or a cloud session.
396
+ if (evalMode || cloudSession) {
377
397
  pi.registerTool(makeTeamReadTool(toolOpts));
378
398
  }
379
399
  // Ticket write-back (spec 2026-08-09): explicit user-intent writes to the
@@ -397,7 +417,12 @@ export async function registerYagni(pi, deps = {}) {
397
417
  // model escalation, not a judgment read); the consult just runs BLIND —
398
418
  // the existing grounding-free advisor persona + the ask_yagni-free tool
399
419
  // list — so no recorded decision reaches the consult either.
400
- const askAdvisorTool = makeAskAdvisorTool({ state: advisorState, workingLine, childUsage, grounded: grounding });
420
+ const askAdvisorTool = makeAskAdvisorTool({
421
+ state: advisorState,
422
+ workingLine,
423
+ childUsage,
424
+ grounded: grounding,
425
+ });
401
426
  pi.registerTool(askAdvisorTool);
402
427
  // /advise runs the SAME tool, sharing the state handle, so a manual consult
403
428
  // draws on the same cap rather than opening a side channel around it.
@@ -464,7 +489,12 @@ export async function registerYagni(pi, deps = {}) {
464
489
  // driver's delegation identity for the diamond directive (see the
465
490
  // before_agent_start handler below) and widens the tool's fan-out ceiling.
466
491
  const ultraHolder = createUltraHolder();
467
- registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine, childUsage, grounded: grounding });
492
+ registerSubagents(pi, {
493
+ isUltra: () => ultraHolder.get(),
494
+ workingLine,
495
+ childUsage,
496
+ grounded: grounding,
497
+ });
468
498
  // /plugin + /reload-plugins: manage Claude Code marketplace plugins from the
469
499
  // session, driving the launcher's `yagni plugin …` (YAGNI_CLI_PATH).
470
500
  registerPluginPanel(pi, { env: deps.env, runCli: deps.runPluginCli });
@@ -497,7 +527,17 @@ export async function registerYagni(pi, deps = {}) {
497
527
  // client estimate on a throw, a non-2xx, or a malformed body.
498
528
  fetchRunSpend: async (runId, signal) => {
499
529
  try {
500
- const res = await resilientFetch(buildRunSpendUrl(baseUrl, runId), { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, { fetchImpl: authedFetch, signal, policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: COST_FETCH_TIMEOUT_MS, jitterRatio: 0 } });
530
+ const res = await resilientFetch(buildRunSpendUrl(baseUrl, runId), { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, {
531
+ fetchImpl: authedFetch,
532
+ signal,
533
+ policy: {
534
+ maxAttempts: 1,
535
+ backoffBaseMs: 0,
536
+ backoffMaxMs: 0,
537
+ timeoutMs: COST_FETCH_TIMEOUT_MS,
538
+ jitterRatio: 0,
539
+ },
540
+ });
501
541
  if (!res.ok)
502
542
  return null;
503
543
  return parseSpendResponse(await res.json());
@@ -658,7 +698,9 @@ export async function registerYagni(pi, deps = {}) {
658
698
  try {
659
699
  ctx?.ui?.notify?.(summary, "info");
660
700
  }
661
- catch { /* fail-soft */ }
701
+ catch {
702
+ /* fail-soft */
703
+ }
662
704
  logEvent({
663
705
  source: "sandbox",
664
706
  level: "info",
@@ -686,7 +728,10 @@ export async function registerYagni(pi, deps = {}) {
686
728
  }
687
729
  await toolOutcomes.close();
688
730
  });
689
- const footerInvalidateHandle = { invalidateGit: () => { }, requestRender: () => { } };
731
+ const footerInvalidateHandle = {
732
+ invalidateGit: () => { },
733
+ requestRender: () => { },
734
+ };
690
735
  const guardianState = makeGuardianState();
691
736
  // Disabled by the local env override OR the workspace kill switch
692
737
  // (yagni_code.guardian, read from the catalog response at launch). The env
@@ -703,8 +748,12 @@ export async function registerYagni(pi, deps = {}) {
703
748
  // hard-aborting at 15s and preempting the backend's retry. Absent an
704
749
  // advertisement (older backend) this keeps the default.
705
750
  const guardianTimeoutMs = deriveGuardianTimeoutMs(guardianLimits, {
706
- ...(guardianTimeoutAdvertisedMs !== undefined ? { timeoutMs: guardianTimeoutAdvertisedMs } : {}),
707
- ...(guardianMaxAttemptsAdvertised !== undefined ? { maxAttempts: guardianMaxAttemptsAdvertised } : {}),
751
+ ...(guardianTimeoutAdvertisedMs !== undefined
752
+ ? { timeoutMs: guardianTimeoutAdvertisedMs }
753
+ : {}),
754
+ ...(guardianMaxAttemptsAdvertised !== undefined
755
+ ? { maxAttempts: guardianMaxAttemptsAdvertised }
756
+ : {}),
708
757
  });
709
758
  guardianLimits.timeoutMs = guardianTimeoutMs;
710
759
  // YAG-510: Guardian events go to the unified sink under source:"guardian"
@@ -760,7 +809,13 @@ export async function registerYagni(pi, deps = {}) {
760
809
  body: JSON.stringify(body),
761
810
  }, {
762
811
  fetchImpl: authedFetch,
763
- policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
812
+ policy: {
813
+ maxAttempts: 1,
814
+ backoffBaseMs: 0,
815
+ backoffMaxMs: 0,
816
+ timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS,
817
+ jitterRatio: 0,
818
+ },
764
819
  });
765
820
  if (!res.ok) {
766
821
  logEvent({
@@ -841,7 +896,9 @@ export async function registerYagni(pi, deps = {}) {
841
896
  }
842
897
  return { key, value };
843
898
  },
844
- clear: () => { map.clear(); },
899
+ clear: () => {
900
+ map.clear();
901
+ },
845
902
  };
846
903
  };
847
904
  const sandboxedFailures = makeSandboxFailureMap(() => true);
@@ -881,7 +938,12 @@ export async function registerYagni(pi, deps = {}) {
881
938
  ? { rules: [], diagnostics: { warnings: [], neverConsultedTools: [] } }
882
939
  : loadPermissionRules({ cwd: process.cwd() });
883
940
  for (const w of loadedRules.diagnostics.warnings) {
884
- logEvent({ source: "permission-rules", level: "warn", event: "config_warning", fields: { warning: w } });
941
+ logEvent({
942
+ source: "permission-rules",
943
+ level: "warn",
944
+ event: "config_warning",
945
+ fields: { warning: w },
946
+ });
885
947
  }
886
948
  if (loadedRules.diagnostics.neverConsultedTools.length > 0) {
887
949
  logEvent({
@@ -916,8 +978,10 @@ export async function registerYagni(pi, deps = {}) {
916
978
  // A build sandbox's toolchain (spec 2026-10-08 §1): when the harness names a
917
979
  // readable toolchain file, every bash spawn and `!` command runs on the
918
980
  // repository's PATH and variables while this process stays on the image's
919
- // Node. Composed before the sandbox's proxy hook. Absent everywhere else.
920
- const toolchainHook = evalMode ? undefined : toolchainSpawnHookFromEnv(env);
981
+ // Node. Composed before the sandbox's proxy hook. Eval mode too: a legacy
982
+ // build runs in it, and its shell needs the repository's toolchain as much
983
+ // as a lane session's. Absent everywhere else.
984
+ const toolchainHook = toolchainSpawnHookFromEnv(env);
921
985
  const sandboxHandle = evalMode
922
986
  ? null
923
987
  : registerSandbox(pi, {
@@ -954,7 +1018,8 @@ export async function registerYagni(pi, deps = {}) {
954
1018
  // ignores the flag entirely — the command runs wrapped, no ask.
955
1019
  sandboxEscape: (toolName, params) => toolName === "bash" &&
956
1020
  sandboxHandle.manager.initialized &&
957
- params.dangerouslyDisableSandbox === true &&
1021
+ params.dangerouslyDisableSandbox ===
1022
+ true &&
958
1023
  sandboxHandle.settings().allowUnsandboxedCommands !== false,
959
1024
  // The auto-sandbox rung's would-run-wrapped proof: after the gate
960
1025
  // strips the flag, this command must actually be wrapped by
@@ -1031,7 +1096,9 @@ export async function registerYagni(pi, deps = {}) {
1031
1096
  // gate at activation): toggling it mid-session does nothing until the
1032
1097
  // session restarts. When set, the gate seeds from the L1 ladder only,
1033
1098
  // byte-identical to the pre-consult behavior.
1034
- ...(evalMode || env.YAGNI_DISABLE_PREFIX_CONSULT === "1" || env.YAGNI_DISABLE_PREFIX_CONSULT === "true"
1099
+ ...(evalMode ||
1100
+ env.YAGNI_DISABLE_PREFIX_CONSULT === "1" ||
1101
+ env.YAGNI_DISABLE_PREFIX_CONSULT === "true"
1035
1102
  ? {}
1036
1103
  : {
1037
1104
  prefixConsult: {
@@ -1097,7 +1164,13 @@ export async function registerYagni(pi, deps = {}) {
1097
1164
  body: JSON.stringify(body),
1098
1165
  }, {
1099
1166
  fetchImpl: authedFetch,
1100
- policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
1167
+ policy: {
1168
+ maxAttempts: 1,
1169
+ backoffBaseMs: 0,
1170
+ backoffMaxMs: 0,
1171
+ timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS,
1172
+ jitterRatio: 0,
1173
+ },
1101
1174
  });
1102
1175
  if (!res.ok) {
1103
1176
  guardianLogSink({ event: "guardian_error_post_failed", status: res.status });
@@ -1132,25 +1205,27 @@ export async function registerYagni(pi, deps = {}) {
1132
1205
  // registered in eval mode: the gate hides the save option when the dep
1133
1206
  // is absent, so a no-op implementation that would still emit
1134
1207
  // ask_approved_remembered never exists.
1135
- ...(evalMode ? {} : {
1136
- persistUserRule: (ruleString, cwd) => {
1137
- mutateLocalConfig(cwd, (config) => {
1138
- const perms = (config.permissions ?? {});
1139
- const allow = Array.isArray(perms.allow) ? [...perms.allow] : [];
1140
- if (!allow.includes(ruleString))
1141
- allow.push(ruleString);
1142
- perms.allow = allow;
1143
- config.permissions = perms;
1144
- });
1145
- logEvent({
1146
- source: "permission-rules",
1147
- level: "info",
1148
- event: "rule_saved",
1149
- sessionId: env.YAGNI_SESSION_ID ?? undefined,
1150
- fields: { rule: ruleString, destination: "local" },
1151
- });
1152
- },
1153
- }),
1208
+ ...(evalMode
1209
+ ? {}
1210
+ : {
1211
+ persistUserRule: (ruleString, cwd) => {
1212
+ mutateLocalConfig(cwd, (config) => {
1213
+ const perms = (config.permissions ?? {});
1214
+ const allow = Array.isArray(perms.allow) ? [...perms.allow] : [];
1215
+ if (!allow.includes(ruleString))
1216
+ allow.push(ruleString);
1217
+ perms.allow = allow;
1218
+ config.permissions = perms;
1219
+ });
1220
+ logEvent({
1221
+ source: "permission-rules",
1222
+ level: "info",
1223
+ event: "rule_saved",
1224
+ sessionId: env.YAGNI_SESSION_ID ?? undefined,
1225
+ fields: { rule: ruleString, destination: "local" },
1226
+ });
1227
+ },
1228
+ }),
1154
1229
  // Opt-in storage stream (YAG-510). Tier decides what leaves the machine:
1155
1230
  // "off" → nothing (not even sent); "hash" → sha256 + family prefix +
1156
1231
  // metadata, no command content; "raw" → adds client-REDACTED command and
@@ -1197,7 +1272,13 @@ export async function registerYagni(pi, deps = {}) {
1197
1272
  body: JSON.stringify(body),
1198
1273
  }, {
1199
1274
  fetchImpl: authedFetch,
1200
- policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS, jitterRatio: 0 },
1275
+ policy: {
1276
+ maxAttempts: 1,
1277
+ backoffBaseMs: 0,
1278
+ backoffMaxMs: 0,
1279
+ timeoutMs: GUARDIAN_EVENT_TIMEOUT_MS,
1280
+ jitterRatio: 0,
1281
+ },
1201
1282
  });
1202
1283
  if (!res.ok) {
1203
1284
  guardianLogSink({ event: "guardian_event_post_failed", status: res.status });
@@ -1250,13 +1331,26 @@ export async function registerYagni(pi, deps = {}) {
1250
1331
  },
1251
1332
  fetchHeadroom: async (signal) => {
1252
1333
  try {
1253
- const res = await resilientFetch(`${baseUrl}/api/yagni-code/credits`, { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, { fetchImpl: authedFetch, signal, policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: COST_FETCH_TIMEOUT_MS, jitterRatio: 0 } });
1334
+ const res = await resilientFetch(`${baseUrl}/api/yagni-code/credits`, { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, {
1335
+ fetchImpl: authedFetch,
1336
+ signal,
1337
+ policy: {
1338
+ maxAttempts: 1,
1339
+ backoffBaseMs: 0,
1340
+ backoffMaxMs: 0,
1341
+ timeoutMs: COST_FETCH_TIMEOUT_MS,
1342
+ jitterRatio: 0,
1343
+ },
1344
+ });
1254
1345
  if (!res.ok)
1255
1346
  return null;
1256
1347
  const data = (await res.json());
1257
1348
  if (typeof data.remaining !== "number")
1258
1349
  return null;
1259
- return { remaining: data.remaining, unit: typeof data.unit === "string" ? data.unit : "credits" };
1350
+ return {
1351
+ remaining: data.remaining,
1352
+ unit: typeof data.unit === "string" ? data.unit : "credits",
1353
+ };
1260
1354
  }
1261
1355
  catch {
1262
1356
  return null;
@@ -1268,7 +1362,17 @@ export async function registerYagni(pi, deps = {}) {
1268
1362
  fetchSpend: yagniSessionId
1269
1363
  ? async (signal) => {
1270
1364
  try {
1271
- const res = await resilientFetch(buildSpendUrl(baseUrl, yagniSessionId, sessionRunIds()), { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, { fetchImpl: authedFetch, signal, policy: { maxAttempts: 1, backoffBaseMs: 0, backoffMaxMs: 0, timeoutMs: COST_FETCH_TIMEOUT_MS, jitterRatio: 0 } });
1365
+ const res = await resilientFetch(buildSpendUrl(baseUrl, yagniSessionId, sessionRunIds()), { method: "GET", headers: { authorization: `Bearer ${getTokenFn() ?? ""}` } }, {
1366
+ fetchImpl: authedFetch,
1367
+ signal,
1368
+ policy: {
1369
+ maxAttempts: 1,
1370
+ backoffBaseMs: 0,
1371
+ backoffMaxMs: 0,
1372
+ timeoutMs: COST_FETCH_TIMEOUT_MS,
1373
+ jitterRatio: 0,
1374
+ },
1375
+ });
1272
1376
  if (!res.ok)
1273
1377
  return null;
1274
1378
  return parseSpendResponse(await res.json());
@@ -1571,6 +1675,34 @@ export async function registerYagni(pi, deps = {}) {
1571
1675
  // the sandbox to reach bash, so the hatch is the one carve-out.
1572
1676
  sandboxHandle.registerOwnBash();
1573
1677
  }
1678
+ else if (toolchainHook) {
1679
+ // Nothing above takes bash over (eval mode, the classic-rows hatch), so
1680
+ // the toolchain reaches the model's spawns through pi's own bash
1681
+ // definition: its schema and rendering unchanged, its PATH the file's.
1682
+ // Fail-soft like the condensed registration: pi's stock bash remains, and
1683
+ // the trail says which step threw (building the definition from the
1684
+ // user's shell settings, or pi's registry taking it) and why.
1685
+ let step = "definition";
1686
+ try {
1687
+ const bash = toolchainBashToolDefinition(toolchainHook, process.cwd());
1688
+ step = "register";
1689
+ pi.registerTool(bash);
1690
+ }
1691
+ catch (err) {
1692
+ logEvent({
1693
+ source: "toolchain",
1694
+ level: "warn",
1695
+ event: "bash_register_failed",
1696
+ fields: {
1697
+ step,
1698
+ errorClass: err instanceof Error ? err.constructor.name : typeof err,
1699
+ // Scrubbed, then cut. A thrown value that is no Error gives no text:
1700
+ // coercing one can itself throw (a null-prototype object).
1701
+ message: err instanceof Error ? scrubSecrets(err.message).slice(0, 300) : "",
1702
+ },
1703
+ });
1704
+ }
1705
+ }
1574
1706
  // Own the identity + inject live company context (and repo rules) on every
1575
1707
  // turn. The extension loads identically in every pi process this app spawns —
1576
1708
  // the interactive driver AND every `/go` stage child, subagent, and advisor
@@ -1584,15 +1716,8 @@ export async function registerYagni(pi, deps = {}) {
1584
1716
  // Resolved per turn (not once at activation): /ultra can flip the driver
1585
1717
  // between the delegation and diamond identities mid-session.
1586
1718
  const isDriver = isDriverCaller(env);
1587
- // A sandbox session is told what its sandbox has instead of a laptop's
1588
- // `gh` recipes. Both signals are the ones the sandbox already rides: the
1589
- // harness's toolchain file, and eval mode, which is where team_read (the
1590
- // tool the section sends GitHub reads to) is registered. A local eval
1591
- // compare sets eval mode without the harness, so it keeps the laptop's.
1592
- const sandboxSession = evalMode && runsInSandboxHarness(env);
1593
- // A stage child of `yagni go --headless --cloud`: headlessGo sets the env
1594
- // where it parses the flag, and the session is told the cloud sandbox.
1595
- const cloudSession = sandboxSession && env[CLOUD_SESSION_ENV] === "1";
1719
+ // `sandboxSession` and `cloudSession` (above, beside eval mode) pick the
1720
+ // "Your sandbox" section in place of the laptop's.
1596
1721
  pi.on("before_agent_start", (event) => noBranding
1597
1722
  ? undefined
1598
1723
  : {
@@ -1629,7 +1754,11 @@ export async function registerYagni(pi, deps = {}) {
1629
1754
  const sessionIdForLog = () => deps.env?.YAGNI_SESSION_ID;
1630
1755
  pi.on("turn_start", (event) => {
1631
1756
  turnStartMs.set(event.turnIndex, Date.now());
1632
- logTurnLifecycle({ kind: "turn_start", sessionId: sessionIdForLog(), turnIndex: event.turnIndex });
1757
+ logTurnLifecycle({
1758
+ kind: "turn_start",
1759
+ sessionId: sessionIdForLog(),
1760
+ turnIndex: event.turnIndex,
1761
+ });
1633
1762
  });
1634
1763
  pi.on("turn_end", (event) => {
1635
1764
  const startedAt = turnStartMs.get(event.turnIndex);
@@ -1683,8 +1812,8 @@ export async function registerYagni(pi, deps = {}) {
1683
1812
  // only place to detect it and trigger recovery. The regex matches
1684
1813
  // "yagni login" (the backend's auth-error message) but NOT
1685
1814
  // "request_too_large" (YAG-460's overflow marker).
1686
- const isAuthError = /yagni login/i.test(msg.errorMessage)
1687
- && !/request_too_large|context_too_large/i.test(msg.errorMessage);
1815
+ const isAuthError = /yagni login/i.test(msg.errorMessage) &&
1816
+ !/request_too_large|context_too_large/i.test(msg.errorMessage);
1688
1817
  if (isAuthError) {
1689
1818
  lastAuthRecovery = null;
1690
1819
  let rotated = false;
@@ -1738,9 +1867,9 @@ export async function registerYagni(pi, deps = {}) {
1738
1867
  const isContextTooLarge = /request_too_large|context_too_large/i.test(msg.errorMessage);
1739
1868
  if (!isContextTooLarge)
1740
1869
  return;
1741
- const explanation = "The conversation context is too large for the model (request_too_large). "
1742
- + "YAGNI Code is compacting the session and retrying automatically — "
1743
- + "if it does not recover, run /compact or start a new session.";
1870
+ const explanation = "The conversation context is too large for the model (request_too_large). " +
1871
+ "YAGNI Code is compacting the session and retrying automatically — " +
1872
+ "if it does not recover, run /compact or start a new session.";
1744
1873
  if (ctx.hasUI) {
1745
1874
  try {
1746
1875
  ctx.ui.notify(explanation, "warning");
@@ -1997,7 +2126,7 @@ export async function registerYagni(pi, deps = {}) {
1997
2126
  // not (an allowed command failing the same way ten times is denied).
1998
2127
  if (command) {
1999
2128
  permissionGate.recordBashOutcome(command, event.isError === true, (event.content ?? [])
2000
- .map((c) => (c && typeof c === "object" && "text" in c ? String(c.text) : ""))
2129
+ .map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "")
2001
2130
  .join("\n"));
2002
2131
  }
2003
2132
  // The auto-sandbox rung's prior-failure evidence: a SANDBOXED bash
@@ -2006,12 +2135,9 @@ export async function registerYagni(pi, deps = {}) {
2006
2135
  // auto-sandboxing into the same denial forever. Only sandboxed runs
2007
2136
  // count (an escaped run's failure text proves nothing about the
2008
2137
  // sandbox); only the trimmed command is kept, never logged.
2009
- if (sandboxHandle &&
2010
- !input.dangerouslyDisableSandbox &&
2011
- event.isError &&
2012
- command) {
2138
+ if (sandboxHandle && !input.dangerouslyDisableSandbox && event.isError && command) {
2013
2139
  const text = (event.content ?? [])
2014
- .map((c) => (c && typeof c === "object" && "text" in c ? String(c.text) : ""))
2140
+ .map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "")
2015
2141
  .join("\n");
2016
2142
  if (isSandboxDenialOutput(text)) {
2017
2143
  recordSandboxedFailure(command);
@@ -2064,9 +2190,7 @@ export async function registerYagni(pi, deps = {}) {
2064
2190
  // (a tool's own failure text). Keep the always-on trail content-free: use
2065
2191
  // ONLY the Error subclass name, never the message or a String() of the
2066
2192
  // result's error payload.
2067
- const errorClass = event.result instanceof Error
2068
- ? event.result.name || "Error"
2069
- : "tool_error";
2193
+ const errorClass = event.result instanceof Error ? event.result.name || "Error" : "tool_error";
2070
2194
  logEvent({
2071
2195
  source: "tool",
2072
2196
  level: "error",
@@ -2103,8 +2227,8 @@ export { recordDecision } from "./recordDecisionTool.js";
2103
2227
  // free text first-class) + the explicit /setup-team drafting flow.
2104
2228
  export { runInitPass, runTeamSetup, registerTeamSetupCommand, isFreshWorkspace, readRepoIntake, draftEngineering, summarizeDraft, FRESH_BRIEF_MIN_CHARS, } from "./initPass.js";
2105
2229
  // Onramp Door B (F2a): the one-time init-pass idempotency marker.
2106
- export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest } from "./initDone.js";
2107
- export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA, BRAND_NAME } from "./branding.js";
2230
+ export { isInitDone, markInitDone, initDoneMarkerFile, _setInitDoneHomeForTest, } from "./initDone.js";
2231
+ export { brandSystemPrompt, YAGNI_IDENTITY, YAGNI_IDENTITY_DRIVER, YAGNI_IDENTITY_ULTRA, BRAND_NAME, } from "./branding.js";
2108
2232
  // YAG-506: user-configurable lifecycle hooks.
2109
2233
  export { loadHooksConfig, makeHookRunner, registerHooks, matchesMatcher, parsePreToolUseOutput, parsePermissionRequestOutput, parseAdditionalContext, parseCompactCancel, HOOK_SUPPORTED_EVENTS, } from "./hooks.js";
2110
2234
  // Ultra mode (/ultra): the aggressive fan-out/verify/synthesize dial.
@@ -2118,7 +2242,7 @@ export { runPipeline } from "./pipeline/orchestrator.js";
2118
2242
  export { HEADLESS_GO_EXIT, HEADLESS_GO_USAGE, parseHeadlessGoArgs, runHeadlessGo, validateHeadlessGoArgs, } from "./pipeline/headlessGo.js";
2119
2243
  // Mission mode: the pure rules for entering the pipeline on an already-approved
2120
2244
  // plan (map/plan skipped, memo as the stand-in repo brief, FINISH not ours).
2121
- export { isMissionMode, missionSeed, missionSkippedStages, normalizeMission } from "./pipeline/mission.js";
2245
+ export { isMissionMode, missionSeed, missionSkippedStages, normalizeMission, } from "./pipeline/mission.js";
2122
2246
  // The eval/smoke tier ceiling (YAGNI_GO_TIER_CAP), clamped centrally in runStage.
2123
2247
  export { clampTier, parseTierCap, resolveTierCap, TIER_CAP_ENV } from "./pipeline/tierCap.js";
2124
2248
  // R1: the in-loop resilience HOF a dev can compose with (or replace at) the
@@ -2126,9 +2250,9 @@ export { clampTier, parseTierCap, resolveTierCap, TIER_CAP_ENV } from "./pipelin
2126
2250
  export { withResilience, classifyTransient, composeAbortSignal } from "./pipeline/resilience.js";
2127
2251
  export { DEFAULT_RESILIENCE_POLICY } from "./pipeline/types.js";
2128
2252
  // P4: the deterministic verify gate seam (override the command via .yagni-code/verify).
2129
- export { buildVerifyEnv, detectVerifyCommand, makeRunVerify, parseVerifyFailures } from "./pipeline/verify.js";
2253
+ export { buildVerifyEnv, detectVerifyCommand, makeRunVerify, parseVerifyFailures, } from "./pipeline/verify.js";
2130
2254
  // M6: the report-only grounded-vs-blind eval engine.
2131
- export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport } from "./pipeline/eval.js";
2255
+ export { blindStages, reportOnlyStages, makeGroundedVsBlindEval, formatComparisonReport, } from "./pipeline/eval.js";
2132
2256
  export { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
2133
2257
  // The general subagent tool: Claude Code-format agent discovery + fan-out.
2134
2258
  export { registerSubagents, makeSubagentTool, discoverSubagents, parseAgentMarkdown, buildSubagentStage, formatAgentList, mapModelTier, SUBAGENT_TOOL_NAME, GENERAL_AGENT_NAME, MAX_PARALLEL_SUBAGENTS, MAX_PARALLEL_SUBAGENTS_ULTRA, DEFAULT_SUBAGENT_TOOLS, } from "./subagents.js";
@@ -18,7 +18,7 @@
18
18
  * @yagni/shared, so it reads the same `{ version: 1, path, env, tools }` with
19
19
  * its own parser. A change there is a change here.
20
20
  */
21
- import { type BashOperations, type BashSpawnHook } from "@earendil-works/pi-coding-agent";
21
+ import { createBashToolDefinition, type BashOperations, type BashSpawnHook } from "@earendil-works/pi-coding-agent";
22
22
  import { type SinkEvent } from "../errorSink.js";
23
23
  export interface ToolchainFile {
24
24
  version: 1;
@@ -51,8 +51,8 @@ export declare function composeSpawnHooks(...hooks: Array<BashSpawnHook | undefi
51
51
  /**
52
52
  * This process runs under the sandbox harness (`infra/e2b/pi/pi-harness.sh`
53
53
  * exports `YAGNI_TOOLCHAIN_FILE` on every E2B and Machines session, Reeve's
54
- * and Wright's alike, and `/go` children inherit it). A developer's own
55
- * terminal never sets it.
54
+ * and Wright's alike, the one-attempt lane's launch line does too, and `/go`
55
+ * children inherit it). A developer's own terminal never sets it.
56
56
  */
57
57
  export declare function runsInSandboxHarness(env?: NodeJS.ProcessEnv): boolean;
58
58
  /**
@@ -61,6 +61,16 @@ export declare function runsInSandboxHarness(env?: NodeJS.ProcessEnv): boolean;
61
61
  * unreadable logs once.
62
62
  */
63
63
  export declare function toolchainSpawnHookFromEnv(env?: NodeJS.ProcessEnv, log?: Log): BashSpawnHook | undefined;
64
+ /**
65
+ * pi's own bash tool as its session builds it (the user's shell and command
66
+ * prefix), with the hook on every spawn. For a session where no other
67
+ * registration takes bash over (eval mode, the classic-rows hatch): its name,
68
+ * description, parameters and rendering stay pi's, and only the spawn's PATH
69
+ * and variables change. The shell settings are the user's own, never the
70
+ * repository's: pi's session reads a repository's settings only once the
71
+ * folder is trusted, and nothing in a sandbox trusts one.
72
+ */
73
+ export declare function toolchainBashToolDefinition(hook: BashSpawnHook, cwd: string): ReturnType<typeof createBashToolDefinition>;
64
74
  /**
65
75
  * `user_bash` (`!` commands) through pi's own local backend, with the hook's
66
76
  * environment: the same toolchain the model's bash tool gets.
@@ -19,7 +19,7 @@
19
19
  * its own parser. A change there is a change here.
20
20
  */
21
21
  import { readFileSync } from "node:fs";
22
- import { createLocalBashOperations, SettingsManager, } from "@earendil-works/pi-coding-agent";
22
+ import { createBashToolDefinition, createLocalBashOperations, getAgentDir, SettingsManager, } from "@earendil-works/pi-coding-agent";
23
23
  import { logEvent } from "../errorSink.js";
24
24
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
25
25
  /** The file, or null when the text is not a well-formed version 1 file. */
@@ -70,12 +70,22 @@ export function readToolchainFile(path, log = logEvent) {
70
70
  }
71
71
  catch (err) {
72
72
  const code = isRecord(err) && typeof err.code === "string" ? err.code : "read_failed";
73
- log({ source: "toolchain", level: "warn", event: "file_unreadable", fields: { path, reason: code } });
73
+ log({
74
+ source: "toolchain",
75
+ level: "warn",
76
+ event: "file_unreadable",
77
+ fields: { path, reason: code },
78
+ });
74
79
  return null;
75
80
  }
76
81
  const file = parseToolchainFile(text);
77
82
  if (!file)
78
- log({ source: "toolchain", level: "warn", event: "file_unreadable", fields: { path, reason: "malformed" } });
83
+ log({
84
+ source: "toolchain",
85
+ level: "warn",
86
+ event: "file_unreadable",
87
+ fields: { path, reason: "malformed" },
88
+ });
79
89
  return file;
80
90
  }
81
91
  /**
@@ -100,8 +110,8 @@ export function composeSpawnHooks(...hooks) {
100
110
  /**
101
111
  * This process runs under the sandbox harness (`infra/e2b/pi/pi-harness.sh`
102
112
  * exports `YAGNI_TOOLCHAIN_FILE` on every E2B and Machines session, Reeve's
103
- * and Wright's alike, and `/go` children inherit it). A developer's own
104
- * terminal never sets it.
113
+ * and Wright's alike, the one-attempt lane's launch line does too, and `/go`
114
+ * children inherit it). A developer's own terminal never sets it.
105
115
  */
106
116
  export function runsInSandboxHarness(env = process.env) {
107
117
  return Boolean(env.YAGNI_TOOLCHAIN_FILE?.trim());
@@ -118,6 +128,23 @@ export function toolchainSpawnHookFromEnv(env = process.env, log = logEvent) {
118
128
  const file = readToolchainFile(path, log);
119
129
  return file ? makeToolchainSpawnHook(file) : undefined;
120
130
  }
131
+ /**
132
+ * pi's own bash tool as its session builds it (the user's shell and command
133
+ * prefix), with the hook on every spawn. For a session where no other
134
+ * registration takes bash over (eval mode, the classic-rows hatch): its name,
135
+ * description, parameters and rendering stay pi's, and only the spawn's PATH
136
+ * and variables change. The shell settings are the user's own, never the
137
+ * repository's: pi's session reads a repository's settings only once the
138
+ * folder is trusted, and nothing in a sandbox trusts one.
139
+ */
140
+ export function toolchainBashToolDefinition(hook, cwd) {
141
+ const settings = SettingsManager.create(cwd, getAgentDir(), { projectTrusted: false });
142
+ return createBashToolDefinition(cwd, {
143
+ shellPath: settings.getShellPath(),
144
+ commandPrefix: settings.getShellCommandPrefix(),
145
+ spawnHook: hook,
146
+ });
147
+ }
121
148
  /**
122
149
  * `user_bash` (`!` commands) through pi's own local backend, with the hook's
123
150
  * environment: the same toolchain the model's bash tool gets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.6-staging.2011.1",
3
+ "version": "1.2.6-staging.2020.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)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "898cdc35e3f5a6cfce58e4c4fcc8b95a9e0a5386"
61
+ "yagniSourceSha": "e3bb7574f09420b1c1a9a25a971db7ae109f468f"
62
62
  }