@gajae-code/coding-agent 0.14.2-nightly.20260818150120.32333814007.gd49c40a6c3c0 → 0.14.2

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/types/cli/crash-cli.d.ts +12 -0
  3. package/dist/types/commands/crash.d.ts +3 -2
  4. package/dist/types/commands/ralplan.d.ts +3 -0
  5. package/dist/types/config/settings-schema.d.ts +19 -0
  6. package/dist/types/crash/index-store.d.ts +6 -0
  7. package/dist/types/crash/upstream/dsn.d.ts +22 -0
  8. package/dist/types/crash/upstream/envelope.d.ts +38 -0
  9. package/dist/types/crash/upstream/relay.d.ts +138 -0
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +16 -0
  11. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  12. package/dist/types/sdk/broker/broker.d.ts +8 -0
  13. package/dist/types/sdk/broker/discovery.d.ts +1 -0
  14. package/dist/types/sdk/bus/notification-service.d.ts +2 -10
  15. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  16. package/dist/types/session/provider-safety-stop-hint.d.ts +79 -0
  17. package/dist/types/session/session-manager.d.ts +17 -1
  18. package/dist/types/tools/ask.d.ts +8 -0
  19. package/dist/types/tools/index.d.ts +2 -0
  20. package/dist/types/utils/herdr-pane.d.ts +18 -5
  21. package/package.json +7 -7
  22. package/src/cli/config-cli.ts +67 -0
  23. package/src/cli/crash-cli.ts +36 -0
  24. package/src/commands/crash.ts +12 -6
  25. package/src/commands/ralplan.ts +23 -3
  26. package/src/config/model-registry.ts +48 -17
  27. package/src/config/settings-schema.ts +22 -0
  28. package/src/config/settings.ts +13 -2
  29. package/src/crash/index-store.ts +52 -2
  30. package/src/crash/upstream/dsn.ts +89 -0
  31. package/src/crash/upstream/envelope.ts +152 -0
  32. package/src/crash/upstream/relay.ts +451 -0
  33. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  34. package/src/defaults/gjc/skills/ralplan/SKILL.md +10 -9
  35. package/src/extensibility/runtime-skill-discovery.ts +35 -0
  36. package/src/gjc-runtime/ralplan-runtime.ts +243 -41
  37. package/src/gjc-runtime/team-runtime.ts +7 -1
  38. package/src/internal-urls/docs-index.generated.ts +4 -3
  39. package/src/modes/components/assistant-message.ts +24 -0
  40. package/src/modes/controllers/event-controller.ts +1 -0
  41. package/src/modes/interactive-mode.ts +23 -0
  42. package/src/modes/print-mode.ts +5 -0
  43. package/src/modes/utils/ui-helpers.ts +2 -0
  44. package/src/prompts/agent-fragments/ralplan-persistence.md +2 -2
  45. package/src/sdk/broker/broker.ts +115 -14
  46. package/src/sdk/broker/discovery.ts +19 -2
  47. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  48. package/src/sdk/bus/index.ts +36 -3
  49. package/src/sdk/bus/notification-service.ts +126 -30
  50. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  51. package/src/sdk/session.ts +7 -0
  52. package/src/session/agent-session.ts +43 -10
  53. package/src/session/blob-store.ts +10 -1
  54. package/src/session/provider-safety-stop-hint.ts +187 -0
  55. package/src/session/session-manager.ts +121 -10
  56. package/src/tools/ask-contract.ts +70 -1
  57. package/src/tools/ask.ts +8 -0
  58. package/src/tools/descriptors.test.ts +72 -0
  59. package/src/tools/index.ts +2 -0
  60. package/src/tools/subagent.ts +9 -0
  61. package/src/utils/herdr-pane.ts +250 -6
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.14.2] - 2026-08-20
6
+
7
+ ### Added
8
+ - Added an opt-in crash upstream relay for cross-install aggregation. Handled tool errors are captured and relayed to a configurable Sentry upstream; relayed-upstream state transitions are journaled and indexed; and the relay is hardened across the trust boundary (no unsigned provenance), delivery watermarks (no replay races), egress and durability (bounded queues, LRU-bounded handled dedupe, restored fatal-occurrence journaling), and recovery/provenance paths closed by exact-head review. The generated config schema was regenerated after the global-only relay settings. Off by default; enabling requires explicit operator opt-in.
9
+ - Added a manual-switch hint after provider safety stops: when a provider halts a turn for a safety reason, the session now surfaces that the operator can switch models/providers manually instead of leaving the stop unexplained (#4650, #4653).
10
+ - Shift+Tab effort cycling now wins over profile suffixes, and operator-intent effort is distinguished from model-default appends, so explicit effort choices are no longer silently overridden at restore time (#4695, #4700).
11
+
12
+ ### Fixed
13
+ - Fixed worker runtime state under `.gjc/state` being swept into leader-repository auto-checkpoints during team runs; checkpoint classification now excludes it (#4603).
14
+ - Fixed ralplan explicit-target worktree handling end to end: runs bind to the explicit `--worktree-root` instead of fragmenting by writer cwd, a trailing `--worktree-root` with no operand is rejected, HEAD objects are verified and explicit-target writes contained, mutation fails closed on review findings, and `.gjc` is confined before migration and fails closed without procfs.
15
+ - Fixed SDK broker liveness (#4704): a broker that stops publishing while its process stays alive is now terminated; the retained broker heartbeat and watchdog observation run off the JS thread; shutdown and observation no longer hold the retained writer lock; stale watchdog observations are fenced during teardown; startup admission reopens after authority recheck; and retained work is fenced across broker restarts.
16
+ - Fixed agent steering losing or racing user input: a steer that interrupts an in-flight tool is delivered, the remaining abort-race gaps in steer-after-tool-interrupt are closed, and a live subagent await ends when user steering arrives instead of hanging.
17
+ - Fixed rotating agent environment credentials not being reloaded and hardened the rotation path so concurrent readers never observe a partially written credential set.
18
+ - Fixed the config CLI reporting success when a setting was not durably saved, and made retargeted CLI persistence fail closed instead of writing to an unintended target.
19
+ - Fixed session robustness: a fatal resident blob now names the session and leaves a record; the corrupted-sentinel lane the strict check walked past is closed; and the transition materialize lane is bound and the dead invalid-ref guard dropped.
20
+ - Fixed notification/herdr integration: recovery no longer re-quarantines native exchange debris, debris cleanup binds to native identity, pane authority binds to process identity so a nested gjc cannot release its parent pane's agent authority, and the native lint gate is satisfied.
21
+ - Fixed `/usage` not surfacing cached usage for API-key credentials in `getCachedUsageReport` (#4686), and made the account-inventory usage tests hermetic on credential-free hosts (#4663).
22
+ - Fixed the auth gateway rejecting explicit `null` fields on openai-chat requests (#4667).
23
+ - Fixed Grok Build turns stalling by giving the provider the same 300s idle window other long-turn providers use.
24
+ - Fixed skill discovery ignoring `skills.customDirectories`; configured directories are now scanned (#4713).
25
+ - Fixed the escaped-non-ASCII em-dash exemption applying beyond display-only tools (#4627).
26
+ - Fixed deep-interview Round 0 ask calls omitting required intent metadata, which produced HTTP 400 rejections on strict providers (#4651).
27
+ - Fixed SDK terminal-in-progress prompt submissions being dropped past the delivery TTL.
28
+ - Fixed postmortem tests writing into the real crash store; they now isolate into a fresh store.
5
29
  ## [0.14.1] - 2026-08-18
6
30
  - Fixed a resident-cache demotion aborting the turn it was supposed to rescue. The demotion is the salvage that runs *because* the cache already failed, so it materializes a store that is by definition missing blobs — yet it was the one materialize call site that never passed `missingPolicy`, inheriting the fail-closed default. It therefore threw `ResidentBlobMissingError` on the very entry it existed to rescue, the transition never committed, the store stayed broken, and every later turn repeated the same throw out of the `message_end` handler. That handler only `logger.warn`s, so the operator saw a bare `Operation aborted` with every pending tool marked `Tool execution was aborted.` and nothing naming a cache, a blob, or an errno — permanently, until the session was restarted. The salvage now substitutes the same placeholder the persistence path already uses, which never emits a `blob:sha256:` ref, so the invariant the fail-closed policy protects is preserved while the session stays usable. Unrecoverable substitutions are counted in `residentBlobPlaceholderCount` and logged once each with their kind, hash, and the demotion's errno, so silent content loss is visible rather than inferred.
7
31
  - Fixed the resident-cache GC reaping the cache of a *running* session, which made long-lived sessions abort every turn with an unexplained `Operation aborted`. The owner lease stores a process start time purely to detect PID reuse, and derived it from `ps -o lstart=` — a zoneless local wall clock rendered from `/etc/localtime` by `ps`, then bound to a zone by `Date.parse` in the reading runtime. Those are two independent zone resolutions: pinning `LC_ALL`/`LANG` stabilized the *format* but not the *instant*, so a reader whose runtime resolved UTC while `ps` rendered UTC+9 computed a start time nine hours off for the same live PID, declared the owner reused, and deleted its instance directory. Observed in the wild: a test process reaped the live instance directories of two running sessions in the same sweep (captured at 32,400,000 ms of skew, exactly the host offset). Each victim then lost the externalized bodies of its large `edit` snapshots, and every later turn died materializing them. The probe now pins `TZ=UTC` on the `ps` child and parses the render explicitly as GMT, so the value is absolute in any reader; leases carry `startTimeBasis: "utc"`, and a lease written before the pin leaves reuse unproven instead of stale, so upgrading cannot reap a live owner once on the way through.
@@ -5,6 +5,18 @@
5
5
  * `crash/report.ts`, which is the part that carries the consent contract and is
6
6
  * therefore tested against stubbed boundaries.
7
7
  */
8
+ import type { Settings } from "../config/settings";
8
9
  import { type CrashReportOutcome } from "../crash/report";
10
+ import { type CrashRelayOutcome } from "../crash/upstream/relay";
9
11
  export declare function runCrashReportCommand(): Promise<CrashReportOutcome>;
10
12
  export declare function runCrashListCommand(json: boolean): Promise<void>;
13
+ /**
14
+ * Batch-relay every due signature to the configured upstream.
15
+ *
16
+ * The startup relay is silent and best-effort; this is the loud form, so a
17
+ * refusal is reported as a refusal instead of being swallowed. It shares the
18
+ * same gate: with `crashReport.upstream` off, it explains that and exits
19
+ * non-zero rather than quietly doing nothing.
20
+ */
21
+ export declare function crashRelayExitCode(outcome: CrashRelayOutcome): number;
22
+ export declare function runCrashRelayCommand(settings: Settings): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `gjc crash` — inspect local crash signatures and file an assisted, fully
3
- * consented bug report. Nothing leaves the machine without an explicit,
4
- * digest-confirmed confirmation for that exact invocation.
3
+ * consented bug report. `gjc crash report` requires an explicit,
4
+ * digest-confirmed confirmation for that exact invocation; `gjc crash relay`
5
+ * uses separately configured standing consent.
5
6
  */
6
7
  import { Command } from "@gajae-code/utils/cli";
7
8
  export default class Crash extends Command {
@@ -1,4 +1,7 @@
1
1
  import { Command } from "@gajae-code/utils/cli";
2
+ import { type RalplanCommandResult } from "../gjc-runtime/ralplan-runtime";
3
+ /** Public CLI path: validate --worktree-root before any settings migration, then dispatch. */
4
+ export declare function runRalplanCliCommand(args: string[], cwd: string): Promise<RalplanCommandResult>;
2
5
  export default class Ralplan extends Command {
3
6
  static description: string;
4
7
  static strict: boolean;
@@ -1841,6 +1841,25 @@ export declare const SETTINGS_SCHEMA: {
1841
1841
  readonly description: "At interactive startup, show one rate-limited line when unreported crash signatures exist. Reads local state only; nothing is ever transmitted without the explicit consent flow in `gjc crash report`.";
1842
1842
  };
1843
1843
  };
1844
+ readonly "crashReport.upstream": {
1845
+ readonly type: "enum";
1846
+ readonly values: readonly ["off", "sentry"];
1847
+ readonly default: "off";
1848
+ readonly ui: {
1849
+ readonly tab: "interaction";
1850
+ readonly label: "Crash Report Upstream (Global)";
1851
+ readonly description: "Global-only opt-in to transmit sanitized crash signatures to an aggregation service for cross-install counting. Off sends nothing. Only fields approved by `sanitizeExternalCrashV1` are sent; prompt text, source code, file contents, and credentials are excluded.";
1852
+ };
1853
+ };
1854
+ readonly "crashReport.upstreamDsn": {
1855
+ readonly type: "string";
1856
+ readonly default: "";
1857
+ readonly ui: {
1858
+ readonly tab: "interaction";
1859
+ readonly label: "Crash Report Upstream DSN (Global)";
1860
+ readonly description: "Global-only Sentry DSN for the opted-in upstream; ignored while Crash Report Upstream is off, and no default destination exists. Only fields approved by `sanitizeExternalCrashV1` are sent; prompt text, source code, file contents, and credentials are excluded.";
1861
+ };
1862
+ };
1844
1863
  readonly collapseChangelog: {
1845
1864
  readonly type: "boolean";
1846
1865
  readonly default: false;
@@ -21,9 +21,15 @@ export interface CrashSignatureEntry {
21
21
  firstSeen: number;
22
22
  lastSeen: number;
23
23
  lastRecordId: string;
24
+ /** Journal-append-order occurrence id, independent of display-time `lastSeen`. */
25
+ lastAppendRecordId?: string;
24
26
  reportedAt?: number;
25
27
  reportedIssueUrl?: string;
26
28
  acknowledgedAt?: number;
29
+ /** Epoch ms this signature was last accepted by the configured upstream. */
30
+ relayedAt?: number;
31
+ /** Occurrence record represented by the durable upstream watermark. */
32
+ relayedRecordId?: string;
27
33
  /** Issues this install already "+1"ed, so re-invocations cannot spam comments. */
28
34
  commentedIssues?: string[];
29
35
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Sentry DSNs are configuration, but accepting a malformed one can turn an
3
+ * opt-in crash relay into an unexpected network destination. Keep parsing
4
+ * deliberately narrow and reconstruct public-key-bearing forms only at the
5
+ * envelope boundary.
6
+ */
7
+ export interface SentryDsn {
8
+ readonly publicKey: string;
9
+ readonly host: string;
10
+ readonly projectId: string;
11
+ readonly envelopeUrl: string;
12
+ }
13
+ /**
14
+ * Parse only the public-key DSN form used by the relay. Secrets, query values,
15
+ * fragments, and plaintext public destinations are all refused before a URL is
16
+ * ever retained as a network target.
17
+ */
18
+ export declare function parseSentryDsn(raw: string): SentryDsn | undefined;
19
+ /** Reconstruct the DSN required in the envelope header, nowhere else. */
20
+ export declare function toDsnString(dsn: SentryDsn): string;
21
+ /** Omit the public key from diagnostics so configuration can be logged safely. */
22
+ export declare function redactDsn(dsn: SentryDsn): string;
@@ -0,0 +1,38 @@
1
+ import { type SentryDsn } from "./dsn";
2
+ export interface CrashEventFrame {
3
+ readonly filename: string;
4
+ readonly function: string;
5
+ }
6
+ export interface BuildCrashEnvelopeInput {
7
+ readonly eventId: string;
8
+ readonly fingerprint: string;
9
+ readonly errorName: string;
10
+ readonly messageClass: string;
11
+ readonly frames: readonly CrashEventFrame[];
12
+ readonly firstSeen: number;
13
+ readonly lastSeen: number;
14
+ readonly lifetimeCount: number;
15
+ readonly release: string;
16
+ readonly platform: string;
17
+ readonly bunVersion: string;
18
+ readonly dsn: SentryDsn;
19
+ /** Upstream severity. Defaults to `fatal`; handled tool failures use `error`. */
20
+ readonly level?: "fatal" | "error";
21
+ }
22
+ export type BuildCrashEnvelopeResult = {
23
+ ok: true;
24
+ body: string;
25
+ eventId: string;
26
+ } | {
27
+ ok: false;
28
+ reason: string;
29
+ };
30
+ /** Create Sentry's required lowercase 128-bit event identifier. */
31
+ export declare function newSentryEventId(): string;
32
+ /** Build the exact auth header used for the hand-rolled envelope POST. */
33
+ export declare function sentryAuthHeader(dsn: SentryDsn, clientVersion: string): string;
34
+ /**
35
+ * Build a three-line event envelope. Sanitization failures and oversize output
36
+ * refuse the complete event: JSON must never be truncated into a new payload.
37
+ */
38
+ export declare function buildCrashEnvelope(input: BuildCrashEnvelopeInput): BuildCrashEnvelopeResult;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Opt-in crash relay to a Sentry-compatible upstream.
3
+ *
4
+ * This is a **second, separate** egress channel from `gjc crash report`. The
5
+ * issue flow keeps its per-invocation, digest-confirmed consent boundary; this
6
+ * one is gated by configuration instead, and is therefore deliberately much
7
+ * narrower in what it can ever emit:
8
+ *
9
+ * 1. `crashReport.upstream` must be `sentry`. The default is `off`, and while
10
+ * it is off this module performs no IO at all — not even a state read.
11
+ * 2. An operator must supply a DSN. No DSN literal is compiled into the binary,
12
+ * so a build has no destination to fall back to; an unset DSN is a hard stop
13
+ * and never means "use ours".
14
+ * 3. Every crash-derived byte must pass `sanitizeExternalCrashV1`. A refusal
15
+ * drops that signature entirely. There is no less-sanitized fallback path.
16
+ *
17
+ * The relay never runs on the fatal path. A crashing process still does exactly
18
+ * one `O_APPEND` write and dies; relaying happens at the *next* startup, after
19
+ * compaction, where blocking and failing are both safe.
20
+ */
21
+ import { type CrashSignatureView, type CrashStatePaths } from "../index-store";
22
+ import { type SentryDsn } from "./dsn";
23
+ /** Trusted environment variable form of the DSN, for CI and one-off runs. */
24
+ export declare const CRASH_UPSTREAM_DSN_ENV = "GJC_CRASH_SENTRY_DSN";
25
+ export interface CrashRelayConfig {
26
+ readonly upstream: "off" | "sentry";
27
+ readonly dsn: string;
28
+ }
29
+ /**
30
+ * The only settings surface the relay is allowed to read.
31
+ *
32
+ * `Settings.get` merges project `.gjc` configuration into the answer, so using
33
+ * it here would let merely opening a repository turn the relay on and choose
34
+ * its destination — an untrusted checkout could redirect crash signatures that
35
+ * were recorded long before it was cloned. Both keys are therefore read from
36
+ * the user/global layer only, which is exactly what `getGlobal` documents
37
+ * itself for.
38
+ */
39
+ export interface TrustedRelaySettings {
40
+ getGlobal(path: "crashReport.upstream" | "crashReport.upstreamDsn"): unknown;
41
+ }
42
+ /**
43
+ * Resolve the relay configuration from the trusted layer.
44
+ *
45
+ * The values are re-validated rather than trusted by type. `getGlobal` reports
46
+ * whatever the hand-editable global config file holds, and it returns
47
+ * `undefined` instead of a schema default, so anything that is not literally
48
+ * `"sentry"` lands on `off` and anything that is not a string lands on an empty
49
+ * DSN. Both absent and malformed therefore fail closed.
50
+ */
51
+ export declare function readTrustedRelayConfig(settings: TrustedRelaySettings): CrashRelayConfig;
52
+ /**
53
+ * The exact shape the relay uses. Narrower than `typeof fetch` on purpose: the
54
+ * relay only ever issues one POST to a known URL, and depending on the full
55
+ * runtime signature (Bun adds `preconnect`) would force every caller and test
56
+ * double to fake surface this module never touches.
57
+ */
58
+ export type CrashRelayFetch = (url: string, init: RequestInit) => Promise<Response>;
59
+ export type CrashRelaySkip = "disabled" | "no-dsn" | "invalid-dsn" | "nothing-to-relay";
60
+ export type CrashRelayOutcome = {
61
+ readonly status: "skipped";
62
+ readonly reason: CrashRelaySkip;
63
+ } | {
64
+ readonly status: "ran";
65
+ /** Signatures the upstream accepted and that are now stamped `relayedAt`. */
66
+ readonly sent: number;
67
+ /** Signatures dropped because the sanitizer refused a field. */
68
+ readonly refused: number;
69
+ /** Signatures the upstream rejected or that failed in transport. */
70
+ readonly failed: number;
71
+ };
72
+ /**
73
+ * Severity of the store being relayed.
74
+ *
75
+ * Fatal crashes and handled tool failures live in separate files on purpose --
76
+ * handled errors are high-volume and would otherwise evict the rare, precious
77
+ * fatal records from a shared cap. They are relayed through the same code with
78
+ * the same egress contract, and differ upstream only by `level`, so a single
79
+ * project can hold both without the noisy class drowning the signal.
80
+ */
81
+ export type CrashRelaySeverity = "fatal" | "error";
82
+ export interface CrashRelayOptions {
83
+ readonly config: CrashRelayConfig;
84
+ readonly paths?: CrashStatePaths;
85
+ readonly handledPaths?: CrashStatePaths;
86
+ readonly severity?: CrashRelaySeverity;
87
+ readonly env?: Record<string, string | undefined>;
88
+ readonly fetchImpl?: CrashRelayFetch;
89
+ readonly now?: () => number;
90
+ readonly maxPerRun?: number;
91
+ readonly platform?: string;
92
+ readonly release?: string;
93
+ readonly bunVersion?: string;
94
+ }
95
+ /**
96
+ * Resolve the destination. Explicit config wins over the environment so a
97
+ * machine-wide export cannot silently redirect a configured install.
98
+ */
99
+ export declare function resolveRelayDsn(config: CrashRelayConfig, env?: Record<string, string | undefined>): {
100
+ ok: true;
101
+ dsn: SentryDsn;
102
+ } | {
103
+ ok: false;
104
+ reason: CrashRelaySkip;
105
+ };
106
+ /**
107
+ * A signature is due when new journal-append-order occurrences exist after the
108
+ * last durable watermark. `lastSeen` is display-time only and must not hide a
109
+ * backdated occurrence. Legacy indexes that only have `relayedAt` stay covered
110
+ * when that wall-clock stamp still covers `lastSeen`. After a downgrade that
111
+ * advanced `relayedAt` without rewriting `relayedRecordId`, the same lastSeen
112
+ * coverage applies only when the latest append is also the lastSeen record.
113
+ */
114
+ export declare function isRelayDue(signature: CrashSignatureView): boolean;
115
+ /**
116
+ * Automatic relay stores use the normal XDG state location only when the XDG
117
+ * value came from a trusted environment. `dirs.ts` rejects checkout `.env`
118
+ * declarations before resolving these paths, including dotenv-expanded values.
119
+ */
120
+ export declare function resolveTrustedRelayStatePaths(): CrashStatePaths;
121
+ export declare function resolveTrustedHandledRelayStatePaths(): CrashStatePaths;
122
+ /**
123
+ * Relay every due signature once. Never throws: a broken upstream, an offline
124
+ * machine or a corrupt crash log must not be able to take down startup.
125
+ */
126
+ export declare function relayCrashSignatures(options: CrashRelayOptions): Promise<CrashRelayOutcome>;
127
+ /** Paths of the handled-error store. Never XDG: a supplied directory is joined directly. */
128
+ export declare function resolveHandledErrorStatePaths(agentDir?: string): CrashStatePaths;
129
+ /**
130
+ * Relay both stores in one pass.
131
+ *
132
+ * Fatal crashes go first: they are rarer and more valuable, so when the
133
+ * per-run cap binds they must not be starved by a noisy handled-error class.
134
+ * The cap is shared across both stores. The gate is evaluated once by the
135
+ * first call, and a skip there means the same skip applies to the second, so
136
+ * `off` still performs no IO at all.
137
+ */
138
+ export declare function relayAllSignatures(options: CrashRelayOptions): Promise<CrashRelayOutcome>;
@@ -155,6 +155,22 @@ export declare function resolveRalplanMaxReviewPassesPerLane(cwd: string, agentD
155
155
  source: string;
156
156
  }>;
157
157
  export declare function isRalplanArtifactWriteInvocation(args: readonly string[]): boolean;
158
+ /**
159
+ * Explicit target-worktree routing (#4693). `--worktree-root <path>` selects the
160
+ * canonical git worktree that owns every ralplan persistence root (run state,
161
+ * stage artifacts, index ledger, HUD state, stuck markers, and review-budget
162
+ * accounting) regardless of the writer process's ambient cwd. Without the flag,
163
+ * behavior stays cwd-based. The invoking cwd is retained only for resolving a
164
+ * relative `--artifact` input file.
165
+ */
166
+ interface RalplanTargetRoot {
167
+ /** Canonical persistence root: the invocation cwd, or the --worktree-root target. */
168
+ root: string;
169
+ /** True when the caller explicitly selected the target via --worktree-root. */
170
+ explicit: boolean;
171
+ }
172
+ export declare function resolveRalplanTargetRoot(args: readonly string[], invocationCwd: string): Promise<RalplanTargetRoot>;
173
+ export declare function assertExplicitTargetGjcNotSymlinked(root: string): Promise<void>;
158
174
  export declare function runNativeRalplanCommand(args: string[], cwd?: string, options?: {
159
175
  agentDir?: string;
160
176
  }): Promise<RalplanCommandResult>;
@@ -1,5 +1,6 @@
1
1
  import type { AssistantMessage, ImageContent, Usage } from "@gajae-code/ai/core";
2
2
  import { Container } from "@gajae-code/tui";
3
+ import { type ProviderSafetyStopHintSession } from "../../session/provider-safety-stop-hint";
3
4
  interface AssistantMessageUpdateOptions {
4
5
  streaming?: boolean;
5
6
  }
@@ -11,7 +12,8 @@ export declare class AssistantMessageComponent extends Container {
11
12
  private hideThinkingBlock;
12
13
  private readonly onImageUpdate?;
13
14
  private readonly onVisibleMutation?;
14
- constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, onImageUpdate?: (() => void) | undefined, viewportAnchorId?: string, onVisibleMutation?: (() => void) | undefined);
15
+ private readonly hintSession?;
16
+ constructor(message?: AssistantMessage, hideThinkingBlock?: boolean, onImageUpdate?: (() => void) | undefined, viewportAnchorId?: string, onVisibleMutation?: (() => void) | undefined, hintSession?: ProviderSafetyStopHintSession | undefined);
15
17
  invalidate(): void;
16
18
  setHideThinkingBlock(hide: boolean): void;
17
19
  setToolResultImages(toolCallId: string, images: ImageContent[]): void;
@@ -233,6 +233,14 @@ export declare function setTerminalPersistenceHookForTest(broker: Broker, hook:
233
233
  export declare function setAmbiguityGraceForTest(broker: Broker, graceMs: number | undefined): void;
234
234
  /** Test-only hook for shortening the startup lock-artifact reap bound. */
235
235
  export declare function setLockArtifactGraceForTest(broker: Broker, graceMs: number | undefined): void;
236
+ /** Test-only hook for shortening the bounded liveness deadline. */
237
+ export declare function setLivenessGraceForTest(broker: Broker, graceMs: number | undefined): void;
238
+ /**
239
+ * Test-only hook for stalling the heartbeat write, reproducing a publication tick
240
+ * whose awaited IO does not settle. Clearing it releases the stalled tick the way
241
+ * recovered IO would, instead of abandoning it forever.
242
+ */
243
+ export declare function setHeartbeatStallForTest(broker: Broker, stalled: boolean): void;
236
244
  /** Test-only hook for forcing the observation the publication watchdog sees. */
237
245
  export declare function setPublicationObservationForTest(broker: Broker, observation: BrokerPublicationObservation | undefined): void;
238
246
  export {};
@@ -3,6 +3,7 @@ import { SDK_STATE_VERSION } from "./state-version";
3
3
  export type BrokerPublicationObservation = "owned" | "absent" | "replaced" | "ambiguous";
4
4
  export interface RetainedBrokerDiscovery {
5
5
  observe(): BrokerPublicationObservation;
6
+ observeAsync(): Promise<BrokerPublicationObservation>;
6
7
  heartbeat(heartbeatAt: number): Promise<boolean>;
7
8
  close(): void;
8
9
  }
@@ -51,16 +51,6 @@ export interface NotificationExactUnlinkResult {
51
51
  export declare function readNotificationEndpointFile(file: string): Promise<NotificationEndpointFile>;
52
52
  /** Bump when the native exact-deletion identity contract changes. */
53
53
  export declare const NATIVE_PATH_IDENTITY_CONTRACT_VERSION = 1;
54
- /**
55
- * Remove exactly the inspected regular file.
56
- *
57
- * Intermediate directory symlinks (e.g. multi-account layouts that share
58
- * `notifications/` via a directory symlink) are resolved before the native
59
- * no-follow unlink. Only the parent path is canonicalized; the final basename
60
- * is rejoined so native exact_unlink still applies AT_SYMLINK_NOFOLLOW to the
61
- * final path component at mutation time. A final-component file symlink stays
62
- * `reparse_point` (including TOCTOU replacement after JS preflight).
63
- */
64
54
  export declare function exactUnlinkNotificationFile(file: string, identity: NotificationEndpointFileIdentity, quarantineName: string): NotificationExactUnlinkResult;
65
55
  /** Minimal filesystem surface the service needs; injectable for tests. */
66
56
  export interface NotificationServiceFs {
@@ -68,6 +58,8 @@ export interface NotificationServiceFs {
68
58
  readFile(file: string, encoding: "utf8"): Promise<string>;
69
59
  readEndpointFile(file: string): Promise<NotificationEndpointFile>;
70
60
  exactUnlink(file: string, identity: NotificationEndpointFileIdentity): Promise<NotificationExactUnlinkResult>;
61
+ /** Identity-bound direct unlink reserved for inert exchange debris. */
62
+ unlinkExact(file: string, identity: NotificationEndpointFileIdentity): Promise<NotificationExactUnlinkResult>;
71
63
  unlink(file: string): Promise<void>;
72
64
  writeFile?(file: string, data: string, opts?: WriteFileOptions): Promise<void>;
73
65
  stat?(file: string): Promise<{
@@ -269,7 +269,7 @@ export declare const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
269
269
  * Generation 169 delivers every ring-positioned session event live through the
270
270
  * bounded, capability-gated directed subscriber leg used by replay.
271
271
  */
272
- export declare const DAEMON_GENERATION = 170;
272
+ export declare const DAEMON_GENERATION = 171;
273
273
  /**
274
274
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
275
275
  * requires durable publication receipts and acknowledged attachment dispatch,
@@ -0,0 +1,79 @@
1
+ import type { AssistantMessage, Model } from "@gajae-code/ai/core";
2
+ import { type ModelSelectorValue } from "../config/model-selector-value";
3
+ /**
4
+ * Normalize a model selector for display: strip ANSI escape sequences and
5
+ * control characters, remove Unicode format characters, replace tabs with
6
+ * spaces, and bound the width. One shared implementation so the TUI render and
7
+ * the print-mode stderr render interpolate the identical safe value (#4653
8
+ * review: centralize selector sanitization instead of leaving each surface to
9
+ * bound itself).
10
+ */
11
+ export declare function sanitizeModelSelectorForDisplay(selector: string): string;
12
+ /**
13
+ * Session capabilities the hint resolver reads. Everything is optional so
14
+ * lightweight host/test contexts without model plumbing degrade to the static
15
+ * guidance instead of throwing.
16
+ */
17
+ export interface ProviderSafetyStopHintSession {
18
+ getConfiguredModelChainState?(role: string): {
19
+ entries: readonly string[];
20
+ origin: string;
21
+ identity?: string;
22
+ explicitHead: boolean;
23
+ } | undefined;
24
+ settings?: {
25
+ getModelRole?(role: string): ModelSelectorValue | undefined;
26
+ };
27
+ getAvailableModels?(): Model[];
28
+ }
29
+ /** Whether an assistant message terminated as a provider safety stop (typed or legacy-persisted). */
30
+ export declare function isProviderSafetyStop(message: AssistantMessage): boolean;
31
+ /**
32
+ * The `provider/id` identity of the model that produced an assistant message.
33
+ * The message's own `provider`/`model` fields are the authoritative identity of
34
+ * the refusing attempt (they survive model switches after the error).
35
+ */
36
+ export declare function refusingModelSelector(message: AssistantMessage): string | undefined;
37
+ /**
38
+ * Resolve a safe, valid alternate model selector from the default role's
39
+ * configured chain — one that is NOT the model that refused. Presentation-only:
40
+ * reads configured intent, dispatches nothing, and mutates nothing (#4650).
41
+ *
42
+ * An entry is named only when the authoritative model selector resolver accepts
43
+ * it (`resolveSelector` with `allowInvalidThinkingSelectorFallback: false`, so
44
+ * malformed suffixes like `:bogus` fail closed while route-suffixed IDs keep
45
+ * their exact-ID semantics) AND the concrete model it resolves to differs from
46
+ * the refuser.
47
+ *
48
+ * The returned selector is RECONSTRUCTED from the resolved model — always
49
+ * `provider/id` qualified, never the original entry (#4653 review: the hint
50
+ * resolver runs without usage-preference context, so a bare/fuzzy/glob entry
51
+ * that resolves here can paste-resolve differently under the real `/model`
52
+ * command's usage-preference context; a provider-qualified selector pins the
53
+ * advertised command to exactly the model that was validated). Only a thinking
54
+ * suffix that itself validated is carried over, so the suggested command is one
55
+ * the resolver parses with identical semantics.
56
+ */
57
+ export declare function resolveSafetyStopAlternateSelector(refuserSelector: string | undefined, chain: ModelSelectorValue | readonly string[] | undefined, availableModels: readonly Model[]): string | undefined;
58
+ /**
59
+ * Bounded guidance shown after a provider safety stop: the failure is
60
+ * model-specific, the context need not be discarded, and the session continues
61
+ * after a manual switch. The configured alternate and the canonical manual
62
+ * switch command are named only when a validated alternate was resolved; the
63
+ * hint never claims the alternate is guaranteed to accept the same context.
64
+ */
65
+ export declare function formatProviderSafetyStopHint(alternateSelector: string | undefined): string;
66
+ /**
67
+ * Resolve the hint to display for a terminal provider safety stop, against the
68
+ * session's current configured default chain and available catalog. Returns a
69
+ * hint string for safety stops (static guidance when no valid alternate can be
70
+ * named) and `undefined` for every other error kind, so unrelated errors keep
71
+ * their existing rendering untouched.
72
+ */
73
+ export declare function resolveProviderSafetyStopHint(message: AssistantMessage, session: ProviderSafetyStopHintSession | undefined): string | undefined;
74
+ /**
75
+ * Compose the display error line for a provider safety stop: the provider's own
76
+ * refusal text is always retained verbatim, with the hint appended on a new
77
+ * line. Returns `undefined` for non-safety-stop errors.
78
+ */
79
+ export declare function formatProviderSafetyStopDisplayError(message: AssistantMessage, alternateSelector: string | undefined): string | undefined;
@@ -210,6 +210,14 @@ export declare function transferSessionMessageIdentity(source: AgentMessage[], t
210
210
  export interface ThinkingLevelChangeEntry extends SessionEntryBase {
211
211
  type: "thinking_level_change";
212
212
  thinkingLevel?: string | null;
213
+ /**
214
+ * True only when an operator effort surface (`setThinkingLevelForControl`,
215
+ * Shift+Tab `cycleThinkingLevel`) recorded this entry. Model-driven appends
216
+ * (model-switch `defaultLevel`, temporary model switches, context clears,
217
+ * re-applies after model cycling) leave it unset so `getThinkingScopeForControl`
218
+ * never mints session scope without operator effort intent (issue #4695).
219
+ */
220
+ operatorIntent?: boolean;
213
221
  }
214
222
  export interface ModelChangeEntry extends SessionEntryBase {
215
223
  type: "model_change";
@@ -955,6 +963,14 @@ interface ResidentBlobSentinel {
955
963
  ref: string;
956
964
  }
957
965
  export declare function residentBlobSentinelForTests(kind: ResidentBlobKind, ref: string): ResidentBlobSentinel;
966
+ export declare function assertResidentReferencesResolvableForTests(entries: readonly FileEntry[], textStore: BlobStore, imageStore?: BlobStore, binding?: {
967
+ sessionId?: string;
968
+ sessionFile?: string;
969
+ }): void;
970
+ export declare function materializeResidentEntriesThrowingForTests<T>(entries: T[], textStore: BlobStore, imageStore?: BlobStore, binding?: {
971
+ sessionId?: string;
972
+ sessionFile?: string;
973
+ }): T[];
958
974
  export declare function materializeResidentEntriesForPersistenceForTests<T>(entries: T[], textStore: BlobStore, imageStore?: BlobStore): T[];
959
975
  /** Get recent sessions for display in welcome screen */
960
976
  export declare function getRecentSessions(sessionDir: string, limit?: number, storage?: SessionStorage): Promise<RecentSessionInfo[]>;
@@ -1277,7 +1293,7 @@ export declare class SessionManager {
1277
1293
  */
1278
1294
  appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage | PythonExecutionMessage | FileMentionMessage): string;
1279
1295
  /** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */
1280
- appendThinkingLevelChange(thinkingLevel?: string): string;
1296
+ appendThinkingLevelChange(thinkingLevel?: string, operatorIntent?: boolean): string;
1281
1297
  appendServiceTierChange(serviceTier: ServiceTier | null): string;
1282
1298
  /** Append a mode change as child of current leaf, then advance leaf. Returns entry id. */
1283
1299
  appendModeChange(mode: string, data?: Record<string, unknown>): string;
@@ -87,6 +87,14 @@ export declare class AskTool implements AgentTool<AskParametersSchema, AskToolDe
87
87
  readonly rawArgumentValidation: (arguments_: Record<string, unknown>) => RawArgumentValidationResult;
88
88
  readonly strict = true;
89
89
  readonly loadMode = "discoverable";
90
+ /**
91
+ * The only ask argument fields rendered to the user as display text: the
92
+ * question wording and the option labels. Everything else — ids,
93
+ * deep-interview metadata (intent contracts/reviews, references), workflow
94
+ * gate metadata — is structured or durable and stays subject to the
95
+ * fail-closed escaped-non-ASCII rejection even after the resample budget.
96
+ */
97
+ readonly displaySafeEscapedArgFields: readonly ["questions.question", "questions.options.label"];
90
98
  constructor(session: ToolSession);
91
99
  static createIf(session: ToolSession): AskTool | null;
92
100
  execute(_toolCallId: string, params: AskParams, signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<AskToolDetails>, context?: AgentToolContext): Promise<AgentToolResult<AskToolDetails>>;
@@ -172,6 +172,8 @@ export interface ToolSession {
172
172
  requestForegroundBashBackground?: () => boolean;
173
173
  /** Get the session-owned or inherited async job manager. */
174
174
  getAsyncJobManager?: () => AsyncJobManager | undefined;
175
+ /** Resolves when the session queues user steering (or `signal` aborts) without consuming it; wait-style tools use this to end their observation early. */
176
+ waitForUserSteering?: (signal: AbortSignal) => Promise<void>;
175
177
  /** Get session ID */
176
178
  getSessionId?: () => string | null;
177
179
  /** Get credential-selection session identity. */
@@ -11,6 +11,13 @@ export interface HerdrReportProcess {
11
11
  export interface HerdrReporterOptions {
12
12
  env?: NodeJS.ProcessEnv;
13
13
  which?: (command: string) => string | null;
14
+ /** Identity written into the ownership marker. Injectable so the nested-process
15
+ * case is testable without actually forking. */
16
+ pid?: number;
17
+ /** OS process incarnation probe, injectable for PID-reuse and recovery tests. */
18
+ processIncarnation?: (pid: number) => string | undefined;
19
+ /** Process liveness/incarnation probe, injectable for cross-platform recovery tests. */
20
+ processProbe?: (pid: number) => HerdrProcessProbe;
14
21
  spawn?: (command: string[], options: {
15
22
  env: NodeJS.ProcessEnv;
16
23
  stdin: "ignore";
@@ -18,6 +25,14 @@ export interface HerdrReporterOptions {
18
25
  stderr: "ignore";
19
26
  }) => HerdrReportProcess;
20
27
  }
28
+ export type HerdrProcessProbe = {
29
+ state: "live";
30
+ incarnation: string;
31
+ } | {
32
+ state: "absent";
33
+ } | {
34
+ state: "unverifiable";
35
+ };
21
36
  /** Session event shape consumed by the reporter. Narrow on purpose: the state
22
37
  * machine is driven only by lifecycle transitions, never by message content. */
23
38
  export interface HerdrSessionEvent {
@@ -41,6 +56,9 @@ export interface HerdrReporter {
41
56
  * unverified path scavenged from an install layout is a command this process
42
57
  * would execute, and PATH/`HERDR_BIN_PATH` are the trust boundary Herdr itself
43
58
  * documents.
59
+ *
60
+ * Also returns null when an ancestor gjc already owns this pane, so a nested
61
+ * invocation reports nothing at all.
44
62
  */
45
63
  export declare function resolveHerdrPaneEnvironment(options?: HerdrReporterOptions): HerdrPaneEnvironment | null;
46
64
  /** Build the argv for a state report. Exported for tests. */
@@ -63,11 +81,6 @@ export declare function buildHerdrClearTitleArgs(paneId: string, seq: number): s
63
81
  * instead of flickering to a placeholder during startup or a rename.
64
82
  */
65
83
  export declare function syncHerdrPaneTitle(sessionName: string | undefined, options?: HerdrReporterOptions): void;
66
- /**
67
- * Create a reporter bound to a pane. `subscribe` supplies the session event
68
- * stream and is parameterized so the state machine is testable without a live
69
- * session.
70
- */
71
84
  export declare function createHerdrReporter(paneEnv: HerdrPaneEnvironment, subscribe: (listener: (event: HerdrSessionEvent) => void) => () => void, options?: HerdrReporterOptions): HerdrReporter;
72
85
  /**
73
86
  * Install the Herdr reporter for a running session. No-op outside a Herdr pane.