@wrongstack/core 0.308.7 → 0.309.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.
Files changed (47) hide show
  1. package/dist/coordination/director/director-toolset.d.ts +2 -2
  2. package/dist/coordination/director-mutation-test-tool.d.ts +29 -0
  3. package/dist/coordination/director-tools.d.ts +2 -0
  4. package/dist/coordination/director.d.ts +16 -0
  5. package/dist/coordination/explore-companion.d.ts +9 -6
  6. package/dist/coordination/fleet.d.ts +12 -0
  7. package/dist/coordination/index.d.ts +1 -1
  8. package/dist/coordination/index.js +990 -61
  9. package/dist/coordination/mail-tools.d.ts +10 -6
  10. package/dist/coordination/mailbox-codecs.d.ts +31 -0
  11. package/dist/coordination/multi-agent-coordinator.d.ts +14 -0
  12. package/dist/coordination/multi-agent-timeout.d.ts +11 -1
  13. package/dist/coordination/mutation-engine.d.ts +76 -0
  14. package/dist/coordination/subagent-budget.d.ts +54 -0
  15. package/dist/coordination/subagent-finish.d.ts +78 -0
  16. package/dist/core/index.js +58 -13
  17. package/dist/defaults/index.js +1132 -106
  18. package/dist/execution/index.js +260 -20
  19. package/dist/hq/index.js +45 -5
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1456 -263
  22. package/dist/infrastructure/index.js +22 -3
  23. package/dist/kernel/events/agent-events.d.ts +31 -2
  24. package/dist/models/index.js +11 -1
  25. package/dist/observability/index.js +1 -1
  26. package/dist/plugin/index.js +113 -12
  27. package/dist/prompts/index.js +360 -3
  28. package/dist/security/auto-approve-policy.d.ts +2 -2
  29. package/dist/security/index.js +177 -50
  30. package/dist/security/permission-helpers.d.ts +11 -0
  31. package/dist/security/permission-policy.d.ts +10 -1
  32. package/dist/security/yolo-risk.d.ts +17 -0
  33. package/dist/session-catalog/index.js +32 -2
  34. package/dist/session-catalog/project-server.js +32 -2
  35. package/dist/skills/index.js +39 -6
  36. package/dist/storage/index.js +44 -3
  37. package/dist/types/index.d.ts +1 -1
  38. package/dist/types/index.js +14 -0
  39. package/dist/types/multi-agent.d.ts +15 -0
  40. package/dist/types/provider.d.ts +29 -1
  41. package/dist/types/tool.d.ts +15 -0
  42. package/dist/utils/index.d.ts +1 -0
  43. package/dist/utils/index.js +54 -4
  44. package/dist/utils/terminal-sanitize.d.ts +41 -0
  45. package/dist/utils/tool-subject.d.ts +1 -1
  46. package/instructions/agents/chaos-monkey.md +61 -0
  47. package/package.json +4 -4
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import type { EventBus } from '../kernel/events.js';
22
22
  import type { Context } from '../core/context.js';
23
- import type { MailboxAudience, MailboxMessageType } from './mailbox-types.js';
23
+ import type { MailboxMessageType } from './mailbox-types.js';
24
24
  import { type MailboxResolver } from './mailbox-tool.js';
25
25
  export interface MailToolsOptions {
26
26
  /** How to obtain a Mailbox given the execution Context (tests). */
@@ -75,12 +75,16 @@ export declare function makeMailSendTool(opts?: MailToolsOptions): {
75
75
  required: string[];
76
76
  };
77
77
  execute(input: unknown, ctx: Context): Promise<{
78
- messageId?: never;
79
- to?: never;
80
- summary?: never;
81
78
  ok: boolean;
82
79
  error: string;
83
- from?: never;
80
+ } | {
81
+ error?: never;
82
+ ok: boolean;
83
+ messageId: string;
84
+ from: string;
85
+ to: string;
86
+ strippedFields: string[];
87
+ summary: string;
84
88
  } | {
85
89
  error?: never;
86
90
  ok: boolean;
@@ -128,7 +132,7 @@ export declare function makeMailInboxTool(opts?: MailToolsOptions): {
128
132
  from: string;
129
133
  to: string;
130
134
  type: MailboxMessageType;
131
- audience: MailboxAudience;
135
+ audience: import("./mailbox-message-types.js").MailboxAudience;
132
136
  subject: string;
133
137
  body: string;
134
138
  timestamp: string;
@@ -45,6 +45,37 @@ export declare class MailboxValidationError extends Error {
45
45
  readonly field: string;
46
46
  constructor(code: string, field: string, message: string);
47
47
  }
48
+ /** Fields allowed in a send mutation payload from untrusted callers. */
49
+ export declare const SEND_ALLOWED_FIELDS: ReadonlySet<string>;
50
+ /** Result of {@link filterMailboxSendPayload}. */
51
+ export interface FilteredSendPayload {
52
+ /** Copy of the input containing only allow-listed and trust-relevant keys. */
53
+ payload: Record<string, unknown>;
54
+ /** Keys that were removed, in input order. Empty when nothing was stripped. */
55
+ stripped: string[];
56
+ }
57
+ /**
58
+ * Strip fields that do not belong in a send payload before it reaches the
59
+ * boundary codec or the mailbox store.
60
+ *
61
+ * Senders (hosts, adapters, models) attach fields the mailbox never asked
62
+ * for — debug knobs, client metadata, accidental whole-context dumps. Two
63
+ * failure modes follow without a filter: the strict codec rejects the whole
64
+ * send because of one irrelevant key, or a lenient surface persists the
65
+ * clutter into every recipient's inbox. This function prevents both:
66
+ * irrelevant keys are removed from the payload and reported in `stripped`.
67
+ *
68
+ * It is pure (never mutates the input) and keyed off the same
69
+ * {@link SEND_ALLOWED_FIELDS} set that governs `parseMailboxSendInput`, so
70
+ * the filter and the validator cannot drift apart.
71
+ *
72
+ * Trust-relevant fields (`from`, `sessionAffinity`, see
73
+ * {@link SEND_FORBIDDEN_FIELDS}) are deliberately PASSED THROUGH, never
74
+ * stripped: the codec must reject them loudly as unknown fields. Dropping
75
+ * them here would convert a forgery attempt into a successful,
76
+ * differently-scoped send.
77
+ */
78
+ export declare function filterMailboxSendPayload(input: Record<string, unknown>): FilteredSendPayload;
48
79
  /**
49
80
  * Result of successful send-input parsing.
50
81
  * `type` is the resolved type after default selection and validation.
@@ -150,6 +150,20 @@ export declare class DefaultMultiAgentCoordinator extends EventEmitter implement
150
150
  * calls this itself.
151
151
  */
152
152
  completeTask(result: TaskResult): void;
153
+ /**
154
+ * Ask every RUNNING subagent that opted into `gracefulFinish` to finish its
155
+ * task in its own turn (see coordination/subagent-finish.ts). This is the
156
+ * leader-side entry point for "the leader agent has finished": it delivers
157
+ * an in-band notification between tool batches — never an interrupt, never
158
+ * an abort. Each notified subagent keeps its existing time budget and
159
+ * accelerates; the watchdog still bounds the maximum lifetime.
160
+ *
161
+ * Subagents without the policy opted in are deliberately untouched — their
162
+ * lifecycle remains the legacy watchdog contract.
163
+ *
164
+ * Returns the number of subagents actually notified.
165
+ */
166
+ requestFinish(reason: string): number;
153
167
  private tryDispatchNext;
154
168
  private canDispatch;
155
169
  private takeNextDispatchableTask;
@@ -1,5 +1,6 @@
1
1
  import type { SubagentRunContext, SubagentRunner, TaskSpec } from '../types/multi-agent.js';
2
2
  import { type SubagentBudget } from './subagent-budget.js';
3
+ import type { GracefulFinish } from './subagent-finish.js';
3
4
  export interface ExecuteSubagentWithTimeoutOptions {
4
5
  runner: SubagentRunner;
5
6
  task: TaskSpec;
@@ -8,6 +9,15 @@ export interface ExecuteSubagentWithTimeoutOptions {
8
9
  preemptFraction?: number | undefined;
9
10
  abortSubagent: (subagentId: string) => void;
10
11
  currentSessionId: () => string | undefined;
12
+ /**
13
+ * Model-driven completion policy resolved from the subagent config. When
14
+ * set, crossing the wall-clock deadline does NOT abort the runner: the
15
+ * budget emits `subagent.finish_requested` in-band (folded into the
16
+ * conversation between tool batches) and extends its own ceiling by the
17
+ * grace window. The terminal stop applies only if that window also
18
+ * elapses — the subagent's bounded maximum lifetime.
19
+ */
20
+ gracefulFinish?: GracefulFinish | undefined;
11
21
  }
12
- export declare function executeSubagentWithTimeout({ runner, task, ctx, budget, preemptFraction, abortSubagent, currentSessionId, }: ExecuteSubagentWithTimeoutOptions): Promise<import("../types/multi-agent.js").SubagentRunOutcome>;
22
+ export declare function executeSubagentWithTimeout({ runner, task, ctx, budget, preemptFraction, abortSubagent, currentSessionId, gracefulFinish, }: ExecuteSubagentWithTimeoutOptions): Promise<import("../types/multi-agent.js").SubagentRunOutcome>;
13
23
  //# sourceMappingURL=multi-agent-timeout.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Deterministic mutation-testing engine ("Kaos Maymunu" / Chaos Monkey).
3
+ *
4
+ * Plans and applies classical boundary-condition mutations to TypeScript
5
+ * source — `>` ↔ `>=`, `+` ↔ `-`, boolean negation, `return x` → `return
6
+ * null`. The engine is deliberately regex/token based, NOT AST based: it has
7
+ * zero dependencies, runs synchronously, and every mutation site is
8
+ * re-derivable from (file, mutation id) alone. Callers (the `mutation_test`
9
+ * director tool) compute the plan, hand it to a chaos-monkey subagent that
10
+ * applies/runs/restores in an isolated worktree, and then compare per-mutant
11
+ * test outcomes against this plan.
12
+ *
13
+ * Safety properties the engine guarantees:
14
+ * - `applyMutation` is a pure string transform: given the same source and
15
+ * the same mutation id it always produces the same output.
16
+ * - Mutations are single-site: exactly one token occurrence changes.
17
+ * - Ids are stable across runs (position-anchored, not hash-of-content), so
18
+ * a worktree-chaos agent and the director tool agree on what each id
19
+ * means without exchanging anything but the id list.
20
+ *
21
+ * @module coordination/mutation-engine
22
+ */
23
+ /** Mutation families this engine can plan. */
24
+ export type MutationKind = 'relax-boundary' | 'tighten-boundary' | 'arith-plus-to-minus' | 'arith-minus-to-plus' | 'negate-boolean' | 'return-null';
25
+ export interface MutationPlanItem {
26
+ /** Stable id: `<kind>#<line1based>#<col1based>`. */
27
+ id: string;
28
+ kind: MutationKind;
29
+ /** Project-relative file path, exactly as passed to planMutations. */
30
+ file: string;
31
+ /** 1-based line. */
32
+ line: number;
33
+ /** 1-based column of the mutated token start. */
34
+ column: number;
35
+ /** Original source text at the site. */
36
+ original: string;
37
+ /** Replacement text. */
38
+ replacement: string;
39
+ }
40
+ export interface PlanMutationsOptions {
41
+ /** Hard cap on planned mutants per file. Default 25. */
42
+ maxPerFile?: number | undefined;
43
+ }
44
+ /**
45
+ * Plan mutations for one file's source text.
46
+ *
47
+ * The scan is line-by-line with the file's own line splits preserved so ids
48
+ * stay (line, column) anchored. Mutations inside comments, string literals,
49
+ * and template literals (single- or multi-line, interpolation contents
50
+ * excepted) are filtered out by the cross-line scanner `computeLineMasks`
51
+ * below.
52
+ */
53
+ export declare function planMutations(file: string, source: string, opts?: PlanMutationsOptions): MutationPlanItem[];
54
+ /**
55
+ * Apply one planned mutation to source. Pure: same input → same output.
56
+ * Returns the original source when the site no longer matches (the file has
57
+ * drifted since planning) so callers can treat that as a skipped mutant.
58
+ */
59
+ export declare function applyMutation(source: string, mutation: Pick<MutationPlanItem, 'kind' | 'line' | 'column' | 'original' | 'replacement'>): string;
60
+ /**
61
+ * Parse a structured mutation report emitted by the chaos-monkey subagent
62
+ * (either via `submit_result` or its final text). Tolerant of surrounding
63
+ * prose: the first JSON object containing a `mutants` array wins.
64
+ */
65
+ export declare function parseMutationReport(text: string): {
66
+ mutants: Array<{
67
+ id: string;
68
+ file: string;
69
+ line: number;
70
+ kind: string;
71
+ status: 'killed' | 'survived' | 'skipped' | 'killed-by-hang';
72
+ evidence?: string | undefined;
73
+ }>;
74
+ summary?: string | undefined;
75
+ } | undefined;
76
+ //# sourceMappingURL=mutation-engine.d.ts.map
@@ -63,6 +63,19 @@ export type BudgetNegotiationMode = 'auto' | 'sync';
63
63
  export type BudgetSessionIdSource = string | (() => string | undefined);
64
64
  interface SubagentBudgetOptions {
65
65
  sessionId?: BudgetSessionIdSource | undefined;
66
+ /** Owning subagent id — used to address the graceful-finish event. */
67
+ subagentId?: string | undefined;
68
+ /**
69
+ * Wall-clock enforcement is owned EXCLUSIVELY by the coordinator watchdog
70
+ * (`executeSubagentWithTimeout`). Set for `gracefulFinish` runs: their
71
+ * notify-then-bound lifecycle must not be raced by `checkTimeout()` calls
72
+ * from `tool.progress` heartbeats, which can fire in the window between a
73
+ * deadline crossing and the watchdog's own tick — starting legacy
74
+ * negotiation that either aborts before `subagent.finish_requested` is
75
+ * delivered or grants an extension past the grace deadline (violating the
76
+ * bounded maximum lifetime). Idle-timeout checks still run.
77
+ */
78
+ wallClockWatchdogOwned?: boolean | undefined;
66
79
  }
67
80
  export interface BudgetUsage {
68
81
  iterations: number;
@@ -146,6 +159,43 @@ export declare class SubagentBudget {
146
159
  * This is the single write path for limit mutations so that future
147
160
  * validation or side-effects live in one place (M1). */
148
161
  patchLimits(ext: Partial<BudgetLimits>): void;
162
+ /**
163
+ * Graceful-finish state (see coordination/subagent-finish.ts).
164
+ * `_finishNotified` guards the single in-band emission; `_grace` records a
165
+ * granted working-time extension past the original wall-clock deadline.
166
+ * They are separate because the two callers want different semantics:
167
+ * the watchdog grants grace at the deadline crossing (notify + extend),
168
+ * while an explicit leader-finished request only notifies — a subagent
169
+ * well inside its budget keeps its full legitimate working time and simply
170
+ * accelerates.
171
+ */
172
+ private _finishNotified;
173
+ private _grace;
174
+ /** True once the in-band finish notification has been emitted. */
175
+ get finishNotified(): boolean;
176
+ /** True once a grace window has been granted past the original deadline. */
177
+ get graceGranted(): boolean;
178
+ /**
179
+ * Notify the subagent in-band to finish its task in its own turn:
180
+ * `subagent.finish_requested` is emitted on the wired EventBus and the
181
+ * agent loop folds the notice into the conversation between tool batches.
182
+ * Nothing aborts — this is a notification, never an interrupt.
183
+ *
184
+ * `opts.graceMs` additionally extends the wall-clock ceiling by that window
185
+ * (used by the watchdog at a deadline crossing, so the model gets working
186
+ * time instead of a kill). Omit it to notify without touching the budget —
187
+ * the subagent keeps its existing time budget and just accelerates.
188
+ *
189
+ * Returns `true` when this call did something (emitted the notification
190
+ * and/or granted grace); `false` when there was nothing to do (already
191
+ * notified, grace already granted, no EventBus wired, budget not started).
192
+ */
193
+ notifyFinish(reason: string, opts?: {
194
+ graceMs?: number | undefined;
195
+ }, now?: () => number): boolean;
196
+ /** Epoch ms by which the subagent should have produced its final output,
197
+ * once a grace window was granted. Undefined before that. */
198
+ get finishDeadlineMs(): number | undefined;
149
199
  private iterations;
150
200
  private toolCalls;
151
201
  private tokenInput;
@@ -161,6 +211,10 @@ export declare class SubagentBudget {
161
211
  private lastActivityTime;
162
212
  private _onThreshold;
163
213
  private readonly _sessionId;
214
+ /** Owning subagent id — used to address the graceful-finish event. */
215
+ private readonly _subagentId;
216
+ /** True when only the coordinator watchdog may enforce wall-clock limits. */
217
+ private readonly _wallClockWatchdogOwned;
164
218
  /**
165
219
  * Hard cap on how long `_negotiateExtension` waits for the coordinator to
166
220
  * respond before defaulting to 'stop'. Without this fallback an absent
@@ -0,0 +1,78 @@
1
+ /**
2
+ * subagent-finish — model-driven completion for background subagents.
3
+ *
4
+ * Problem this module solves: a subagent that outlives its leader (the
5
+ * post-session Chimera reviewer is the canonical case) previously had only
6
+ * two exits — finish on its own, or be killed by the wall-clock watchdog
7
+ * (`executeSubagentWithTimeout` aborts the runner at the deadline). The kill
8
+ * is an external interrupt: it discards whatever the model was mid-way
9
+ * through producing and violates the "agent completes its own turn" contract.
10
+ *
11
+ * The graceful-finish path replaces the kill with a notification:
12
+ *
13
+ * 1. The subagent's config opts in via `gracefulFinish`.
14
+ * 2. When the finish condition fires — the wall-clock deadline is crossed,
15
+ * or the leader explicitly calls `Director.requestFinish()` — the budget
16
+ * records a single finish deadline and emits `subagent.finish_requested`
17
+ * on the subagent's EventBus, carrying the ready-to-read notice text.
18
+ * 3. The agent loop folds that notice into the conversation as a `/btw`
19
+ * note at the TOP of its next iteration — between tool batches,
20
+ * in-band, no abort, no restart.
21
+ * 4. The model reads the notice and completes its task in its own turn.
22
+ * 5. If the grace window also elapses, the watchdog applies the existing
23
+ * terminal stop. That is the documented maximum lifetime — the bound
24
+ * that keeps a subagent from living forever, reached only after the
25
+ * model was given legitimate working time to finish.
26
+ *
27
+ * @module subagent-finish
28
+ */
29
+ /** Structural form of `SubagentConfig.gracefulFinish` (avoids a layer cycle). */
30
+ export type GracefulFinishConfig = boolean | {
31
+ graceMs?: number | undefined;
32
+ } | undefined;
33
+ /**
34
+ * EventBus name for the in-band finish request. Emitted on the subagent's own
35
+ * EventBus (the bus the runner wired into `budget._events`), so delivery is
36
+ * process-local and lands at the loop's next iteration boundary.
37
+ */
38
+ export declare const SUBAGENT_FINISH_REQUESTED_EVENT = "subagent.finish_requested";
39
+ /**
40
+ * Default grace window granted after the finish request fires. Long enough
41
+ * for a model to run one or two more tool calls and write a complete final
42
+ * report; short enough that a session shutdown cannot stall indefinitely.
43
+ */
44
+ export declare const DEFAULT_SUBAGENT_FINISH_GRACE_MS = 120000;
45
+ /** Resolved graceful-finish policy for one subagent. */
46
+ export interface GracefulFinish {
47
+ /** Milliseconds of legitimate working time granted after notification. */
48
+ graceMs: number;
49
+ }
50
+ /**
51
+ * Resolve the graceful-finish policy from a `SubagentConfig`.
52
+ *
53
+ * `undefined` (the default for every existing spawn) keeps the legacy
54
+ * behavior byte-for-byte: the watchdog preempts/negotiates/aborts exactly as
55
+ * it did before. Only spawns that explicitly opt in get the notify-then-bound
56
+ * lifecycle.
57
+ */
58
+ export declare function resolveGracefulFinish(config: {
59
+ gracefulFinish?: GracefulFinishConfig | undefined;
60
+ }): GracefulFinish | undefined;
61
+ /**
62
+ * The in-band notice the subagent reads between tool calls. Delivered as a
63
+ * `/btw` note, so it arrives at the next iteration boundary — never mid-tool,
64
+ * never as an abort. The wording is deliberately imperative and
65
+ * self-contained: the model must be able to act on it without any other
66
+ * context about why the leader finished. Carried verbatim on the event
67
+ * payload so the core agent loop needs no coordination-layer import to fold
68
+ * it into the conversation.
69
+ */
70
+ export declare function buildSubagentFinishNotice(input: {
71
+ /** Why the finish was requested (e.g. "leader session ended"). */
72
+ reason: string;
73
+ /** Epoch milliseconds by which the final output should be complete. */
74
+ deadlineMs: number;
75
+ /** Granted working-time window in milliseconds. */
76
+ graceMs: number;
77
+ }): string;
78
+ //# sourceMappingURL=subagent-finish.d.ts.map
@@ -1751,6 +1751,14 @@ var PATTERNS = [
1751
1751
  anchor: "sk-ant-"
1752
1752
  },
1753
1753
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
1754
+ {
1755
+ // `xai` is a first-class provider in this codebase, but its key shape was
1756
+ // absent here — so the one credential format WrongStack itself hands users
1757
+ // was the one the scrubber could not recognize (audit 2026-08-20).
1758
+ type: "xai_key",
1759
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
1760
+ anchor: "xai-"
1761
+ },
1754
1762
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
1755
1763
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
1756
1764
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -1841,8 +1849,8 @@ var PATTERNS = [
1841
1849
  // replacement so the separator between adjacent secrets is preserved
1842
1850
  // rather than collapsed. Capture groups are therefore: 1=leading
1843
1851
  // delimiter, 2=key name, 3=value.
1844
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1845
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
1852
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1853
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
1846
1854
  },
1847
1855
  {
1848
1856
  type: "json_credential_key",
@@ -1959,6 +1967,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
1959
1967
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1960
1968
  var SCRUB_CHUNK_BYTES = 64 * 1024;
1961
1969
  var SCRUB_OVERLAP_BYTES = 1024;
1970
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
1971
+ var PEM_END_MARKER = "-----END";
1972
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
1973
+ var PEM_END_LINE_TOLERANCE = 64;
1974
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
1975
+ const head = text.slice(chunkStart, proposedEnd);
1976
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
1977
+ if (lastBegin === -1) return proposedEnd;
1978
+ const fromBegin = text.slice(chunkStart + lastBegin);
1979
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
1980
+ if (!marker || marker.index !== 0) return proposedEnd;
1981
+ const bodyStart = marker[0].length;
1982
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
1983
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
1984
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
1985
+ return proposedEnd;
1986
+ }
1987
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
1988
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
1989
+ return Math.max(proposedEnd, end);
1990
+ }
1962
1991
  var PATTERN_ANCHORS = [
1963
1992
  ...new Set(
1964
1993
  PATTERNS.flatMap(
@@ -1995,6 +2024,7 @@ var DefaultSecretScrubber = class {
1995
2024
  }
1996
2025
  }
1997
2026
  end = safe === -1 ? end : safe + 1;
2027
+ end = extendChunkBoundaryPastPem(text, i, end);
1998
2028
  }
1999
2029
  out.push(this.scrubOne(text.slice(i, end)));
2000
2030
  i = end;
@@ -3649,9 +3679,17 @@ var TASK_SNIPPET_CHARS = 60;
3649
3679
  function fleetPulseSignature(statuses) {
3650
3680
  return statuses.map((s) => `${s.agentId}|${s.status}|${s.currentTask ?? ""}`).sort().join("\n");
3651
3681
  }
3652
- function peerLine(s) {
3682
+ function visibleLineKey(s) {
3683
+ const role = s.role && s.role !== s.name ? s.role : "";
3684
+ const task = s.currentTask && s.currentTask.length > TASK_SNIPPET_CHARS ? `${s.currentTask.slice(0, TASK_SNIPPET_CHARS)}\u2026` : s.currentTask ?? "";
3685
+ const tool = s.currentTool || "";
3686
+ const toolCalls = s.toolCalls > 0 ? String(s.toolCalls) : "";
3687
+ return [s.name, role, s.status, task, tool, toolCalls].join("\0");
3688
+ }
3689
+ function peerLine(s, count = 1) {
3653
3690
  const role = s.role && s.role !== s.name ? ` (${s.role})` : "";
3654
- const parts = [`\u2022 ${s.name}${role} \u2014 ${s.status}`];
3691
+ const grouped = count > 1 ? ` \xD7${count}` : "";
3692
+ const parts = [`\u2022 ${s.name}${role}${grouped} \u2014 ${s.status}`];
3655
3693
  if (s.currentTask) {
3656
3694
  const task = s.currentTask.length > TASK_SNIPPET_CHARS ? `${s.currentTask.slice(0, TASK_SNIPPET_CHARS)}\u2026` : s.currentTask;
3657
3695
  parts.push(`"${task}"`);
@@ -3667,13 +3705,20 @@ function buildFleetPulseBlock(statuses, opts) {
3667
3705
  if (peers.length === 0) return null;
3668
3706
  const order = { running: 0, streaming: 0, waiting_user: 1, idle: 2, error: 3, offline: 4 };
3669
3707
  const sorted = [...peers].sort(
3670
- (x, y) => (order[x.status] ?? 5) - (order[y.status] ?? 5) || x.name.localeCompare(y.name)
3708
+ (x, y) => (order[x.status] ?? 5) - (order[y.status] ?? 5) || visibleLineKey(x).localeCompare(visibleLineKey(y))
3671
3709
  );
3672
3710
  const shown = sorted.slice(0, maxAgents);
3673
3711
  const hidden = sorted.length - shown.length;
3674
3712
  const parts = [];
3675
3713
  parts.push(`[FLEET PULSE] ${peers.length} peer${peers.length === 1 ? "" : "s"} online:`);
3676
- for (const s of shown) parts.push(peerLine(s));
3714
+ for (let i = 0; i < shown.length; ) {
3715
+ let run = 1;
3716
+ while (i + run < shown.length && visibleLineKey(shown[i]) === visibleLineKey(shown[i + run])) {
3717
+ run++;
3718
+ }
3719
+ parts.push(peerLine(shown[i], run));
3720
+ i += run;
3721
+ }
3677
3722
  if (hidden > 0) parts.push(`\u2026 +${hidden} more`);
3678
3723
  parts.push(
3679
3724
  "[END FLEET PULSE] (FYI \u2014 coordinate via mail_send; avoid duplicating peers' work)"
@@ -6225,6 +6270,7 @@ function maybeAppendPendingNextSteps(ctx, res) {
6225
6270
  // src/prompts/prompt-journal.ts
6226
6271
  import * as fs5 from "node:fs/promises";
6227
6272
  import * as path11 from "node:path";
6273
+ var defaultScrubber2 = new DefaultSecretScrubber();
6228
6274
  async function ensureGitignore(projectRoot) {
6229
6275
  const gitignorePath = path11.join(projectRoot, ".gitignore");
6230
6276
  try {
@@ -6252,7 +6298,9 @@ async function recordPromptJournalEntry(opts) {
6252
6298
  const monthStr = dateStr.slice(0, 7);
6253
6299
  const sessionId = opts.sessionId && opts.sessionId.trim() ? opts.sessionId.trim() : "general";
6254
6300
  const id = `pmt_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
6255
- const content = opts.content ?? "";
6301
+ const content = defaultScrubber2.scrub(opts.content ?? "");
6302
+ const rawContent = typeof opts.rawContent === "string" && opts.rawContent.length > 0 ? defaultScrubber2.scrub(opts.rawContent) : opts.rawContent;
6303
+ const decisionReason = typeof opts.decisionReason === "string" && opts.decisionReason.length > 0 ? defaultScrubber2.scrub(opts.decisionReason) : opts.decisionReason;
6256
6304
  const lines = content.split("\n");
6257
6305
  const characterCount = content.length;
6258
6306
  const lineCount = lines.length;
@@ -6265,7 +6313,7 @@ async function recordPromptJournalEntry(opts) {
6265
6313
  role: opts.role ?? (opts.category === "system_prompt" ? "system" : "user"),
6266
6314
  category: opts.category,
6267
6315
  content,
6268
- rawContent: opts.rawContent,
6316
+ rawContent,
6269
6317
  metadata: {
6270
6318
  model: opts.model,
6271
6319
  provider: opts.provider,
@@ -6276,7 +6324,7 @@ async function recordPromptJournalEntry(opts) {
6276
6324
  activeTools: opts.activeTools,
6277
6325
  contextFiles: opts.contextFiles,
6278
6326
  durationMs: opts.durationMs,
6279
- decisionReason: opts.decisionReason,
6327
+ decisionReason,
6280
6328
  tags: opts.tags
6281
6329
  }
6282
6330
  };
@@ -8876,10 +8924,7 @@ function createAgentToolHandler(a) {
8876
8924
  } catch {
8877
8925
  }
8878
8926
  }
8879
- if (decision === "yes") {
8880
- const p = a.permission;
8881
- p.allowOnce?.({ tool: tool.name, pattern: result.suggestedPattern });
8882
- } else if (decision === "no") {
8927
+ if (decision === "no") {
8883
8928
  const p = a.permission;
8884
8929
  p.denyOnce?.({ tool: tool.name, pattern: result.suggestedPattern });
8885
8930
  }