@wrongstack/core 0.302.2 → 0.305.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chronicle/index.js +117 -30
- package/dist/chronicle/project-server.js +12 -5
- package/dist/coordination/agents/index.js +4645 -3448
- package/dist/coordination/agents/project-agent-auto-optimize.d.ts +116 -0
- package/dist/coordination/agents/project-agent-capture-window.d.ts +29 -0
- package/dist/coordination/agents/project-agent-config-io.d.ts +11 -0
- package/dist/coordination/agents/project-agent-consolidation.d.ts +34 -2
- package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
- package/dist/coordination/agents/project-agent-files.d.ts +12 -3
- package/dist/coordination/agents/project-agent-identity-types.d.ts +4 -0
- package/dist/coordination/agents/project-agent-identity.d.ts +38 -11
- package/dist/coordination/agents/project-agent-learning-entries.d.ts +8 -2
- package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
- package/dist/coordination/agents/project-agent-learning-structured.d.ts +72 -1
- package/dist/coordination/agents/project-agent-optimizer.d.ts +49 -0
- package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
- package/dist/coordination/agents/project-agent-skill-layer.d.ts +146 -0
- package/dist/coordination/agents/role-skills.d.ts +11 -1
- package/dist/coordination/agents/types.d.ts +10 -2
- package/dist/coordination/director-prompts.d.ts +19 -6
- package/dist/coordination/director-tools.d.ts +2 -2
- package/dist/coordination/fleet.d.ts +0 -6
- package/dist/coordination/index.d.ts +1 -1
- package/dist/coordination/index.js +5746 -4326
- package/dist/coordination/mail-tools.d.ts +1 -1
- package/dist/core/agent-types.d.ts +4 -2
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/context.d.ts +19 -0
- package/dist/core/conversation-state.d.ts +14 -0
- package/dist/core/fallback-profile-manager.d.ts +70 -2
- package/dist/core/index.js +317 -108
- package/dist/core/system-prompt-blocks.d.ts +1 -1
- package/dist/core/system-prompt-builder.d.ts +13 -1
- package/dist/core/system-prompt-glossary.d.ts +73 -0
- package/dist/core/system-prompt-memory-skills.d.ts +2 -2
- package/dist/defaults/index.js +1635 -1120
- package/dist/execution/council-orchestrator.d.ts +3 -13
- package/dist/execution/index.js +3136 -2706
- package/dist/execution/one-shot-llm.d.ts +5 -0
- package/dist/goal/index.js +7 -0
- package/dist/hq/index.js +17 -7
- package/dist/hq/protocol/kanban.d.ts +21 -0
- package/dist/hq/protocol.js +5 -1
- package/dist/hq/redaction.d.ts +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5502 -3483
- package/dist/infrastructure/index.js +247 -122
- package/dist/kernel/events/agent-events.d.ts +28 -0
- package/dist/plugin/index.js +111 -7
- package/dist/registry/index.js +11 -0
- package/dist/registry/tool-registry.d.ts +8 -0
- package/dist/replay/hash.d.ts +9 -0
- package/dist/replay/index.js +14 -4
- package/dist/replay/replay-provider-runner.d.ts +31 -1
- package/dist/security/index.js +92 -21
- package/dist/security/kanban-boundary.d.ts +5 -1
- package/dist/security/secret-vault.d.ts +2 -0
- package/dist/session-catalog/index.js +86 -10
- package/dist/session-catalog/project-server.js +100 -17
- package/dist/session-catalog/protocol.d.ts +20 -4
- package/dist/session-catalog/store.d.ts +19 -3
- package/dist/storage/index.js +305 -69
- package/dist/storage/memory-consolidator.d.ts +4 -2
- package/dist/storage/plan-store.d.ts +1 -1
- package/dist/storage/session-resume-validation.d.ts +24 -0
- package/dist/storage/session-store/directory-scan.d.ts +5 -1
- package/dist/storage/session-store/fork-session.d.ts +13 -1
- package/dist/storage/session-store/load-cache.d.ts +11 -0
- package/dist/storage/session-store/prune-helpers.d.ts +5 -0
- package/dist/storage/session-store.d.ts +18 -0
- package/dist/tasking/index.js +5 -0
- package/dist/tools/index.js +1151 -831
- package/dist/types/config/mcp-features.d.ts +31 -1
- package/dist/types/config/root.d.ts +23 -1
- package/dist/types/config/skills-fleet-brain.d.ts +34 -0
- package/dist/types/config/tools.d.ts +22 -0
- package/dist/types/config/ui.d.ts +14 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/default-config.d.ts +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +23 -0
- package/dist/types/multi-agent.d.ts +7 -0
- package/dist/types/session.d.ts +9 -1
- package/dist/types/task-graph.d.ts +2 -0
- package/dist/types/tool-executor.d.ts +2 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +217 -76
- package/dist/utils/project-state-guard.d.ts +21 -0
- package/dist/utils/session-scoped-path.d.ts +17 -0
- package/dist/utils/todos-format.d.ts +20 -0
- package/instructions/leader-after-task.md +3 -4
- package/instructions/system-lite.md +17 -14
- package/instructions/system-pro.md +29 -30
- package/instructions/system.md +29 -28
- package/package.json +3 -3
- package/skills/wrongstack-kanban/SKILL.md +95 -93
|
@@ -139,6 +139,34 @@ export interface AgentEventMap {
|
|
|
139
139
|
*/
|
|
140
140
|
transcriptPath?: string | undefined;
|
|
141
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* A spawn resolved fewer skills than it selected. Emitted so a skill that
|
|
144
|
+
* silently failed to load — missing from the loader, gated by a capability
|
|
145
|
+
* the subagent lacks, or cut by the prompt budget — is observable instead of
|
|
146
|
+
* leaving the agent believing it received guidance it never got.
|
|
147
|
+
*/
|
|
148
|
+
'subagent.skills.dropped': {
|
|
149
|
+
sessionId?: string | undefined;
|
|
150
|
+
role?: string | undefined;
|
|
151
|
+
/** Skills whose body (and project addendum) reached the prompt. */
|
|
152
|
+
selected: string[];
|
|
153
|
+
/** skill → reason it was dropped. */
|
|
154
|
+
dropped: Record<string, string>;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* A background learning-distillation pass finished for a roster role.
|
|
158
|
+
* Emitted by the fleet host's auto-optimize scheduler so surfaces can show
|
|
159
|
+
* that an agent's skills were refined without a user action.
|
|
160
|
+
*/
|
|
161
|
+
'agent.learning.optimized': {
|
|
162
|
+
sessionId?: string | undefined;
|
|
163
|
+
role: string;
|
|
164
|
+
trigger: string;
|
|
165
|
+
status: string;
|
|
166
|
+
/** Skill addenda refreshed by the pass. */
|
|
167
|
+
skills: string[];
|
|
168
|
+
error?: string | undefined;
|
|
169
|
+
};
|
|
142
170
|
'subagent.task_started': {
|
|
143
171
|
/** Parent/host session id. */
|
|
144
172
|
sessionId?: string | undefined;
|
package/dist/plugin/index.js
CHANGED
|
@@ -2443,12 +2443,97 @@ var FallbackProfileManager = class {
|
|
|
2443
2443
|
return this.resolveRefs([ref], exclude);
|
|
2444
2444
|
}
|
|
2445
2445
|
/**
|
|
2446
|
-
* Resolve every usable configured target as
|
|
2446
|
+
* Resolve every usable configured target as a bounded last-resort chain.
|
|
2447
2447
|
* Normal smart defaults stay bounded; callers append this only after the
|
|
2448
|
-
* preferred chain and only when automatic fallback is enabled.
|
|
2448
|
+
* preferred chain and only when automatic fallback is enabled. The cap
|
|
2449
|
+
* ({@link MAX_LAST_RESORT_CANDIDATES}) prevents a config with many providers
|
|
2450
|
+
* from producing a degenerate chain of doomed requests during a systemic
|
|
2451
|
+
* outage — by this point the smart default, bridge, and default profile
|
|
2452
|
+
* have already failed.
|
|
2449
2453
|
*/
|
|
2450
2454
|
resolveAllConfigured(exclude) {
|
|
2451
|
-
return this.smartDefault(exclude,
|
|
2455
|
+
return this.smartDefault(exclude, this.lastResortCap());
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Effective cap for the last-resort append. Reads the user-configurable
|
|
2459
|
+
* {@link Config.fallbackMaxLastResortCandidates} when set and valid;
|
|
2460
|
+
* otherwise falls back to the compiled-in default
|
|
2461
|
+
* {@link MAX_LAST_RESORT_CANDIDATES}.
|
|
2462
|
+
*/
|
|
2463
|
+
lastResortCap() {
|
|
2464
|
+
const configured = this.config.fallbackMaxLastResortCandidates;
|
|
2465
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
|
|
2466
|
+
return Math.floor(configured);
|
|
2467
|
+
}
|
|
2468
|
+
return MAX_LAST_RESORT_CANDIDATES;
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Build the complete fallback candidate chain shared by the agent-loop
|
|
2472
|
+
* extension and the one-shot orchestrator. Centralizes the bridge → primary →
|
|
2473
|
+
* selected → default-profile → all-configured ladder and the fromExplicitSource
|
|
2474
|
+
* gate so both consumers produce identical ordering and depth semantics.
|
|
2475
|
+
*
|
|
2476
|
+
* Layering (each step deduped against all prior):
|
|
2477
|
+
* 1. Bridge (emergency continuity route).
|
|
2478
|
+
* 2. Configured primary, when the live context drifted from it.
|
|
2479
|
+
* 3. The selected chain (explicit refs → named profile → smart default),
|
|
2480
|
+
* via {@link resolveEffective}.
|
|
2481
|
+
* 4. The "default" profile — extra depth, ONLY when the chain was auto-derived.
|
|
2482
|
+
* 5. Every other configured provider — last resort, ONLY when the chain was
|
|
2483
|
+
* auto-derived AND `effectiveFallbackAuto` is true.
|
|
2484
|
+
*
|
|
2485
|
+
* Returns the empty chain when `closedWorld` is true and no explicit
|
|
2486
|
+
* refs/profile resolved — a model allowlist never leaks to unlisted models.
|
|
2487
|
+
*
|
|
2488
|
+
* @internal caller-aware options (primary, closedWorld) are accepted because
|
|
2489
|
+
* the agent loop has context the manager does not own; the resolution
|
|
2490
|
+
* pipeline itself is identical for both callers.
|
|
2491
|
+
*/
|
|
2492
|
+
resolveCandidates(current, opts = {}) {
|
|
2493
|
+
const configuredPrimary = opts.primary ?? {
|
|
2494
|
+
providerId: this.config.provider,
|
|
2495
|
+
model: this.config.model
|
|
2496
|
+
};
|
|
2497
|
+
const configFallbackAuto = this.config.fallbackAuto;
|
|
2498
|
+
const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
|
|
2499
|
+
const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
|
|
2500
|
+
const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
|
|
2501
|
+
const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
|
|
2502
|
+
const fromExplicitSource = explicitUsable || profileUsable;
|
|
2503
|
+
const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
|
|
2504
|
+
fallbackModels: explicitRefs,
|
|
2505
|
+
fallbackProfile: opts.fallbackProfile,
|
|
2506
|
+
fallbackAuto: effectiveFallbackAuto,
|
|
2507
|
+
exclude: current
|
|
2508
|
+
});
|
|
2509
|
+
const candidates = [];
|
|
2510
|
+
if (opts.closedWorld) {
|
|
2511
|
+
candidates.push(...selectedChain);
|
|
2512
|
+
} else {
|
|
2513
|
+
candidates.push(...this.resolveBridge(current));
|
|
2514
|
+
if (!(configuredPrimary.providerId === current.providerId && configuredPrimary.model === current.model)) {
|
|
2515
|
+
candidates.push({
|
|
2516
|
+
providerId: configuredPrimary.providerId,
|
|
2517
|
+
model: configuredPrimary.model,
|
|
2518
|
+
providerSwitched: configuredPrimary.providerId !== current.providerId
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
candidates.push(...selectedChain);
|
|
2522
|
+
if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
|
|
2523
|
+
candidates.push(...this.resolve("default", { exclude: current }));
|
|
2524
|
+
}
|
|
2525
|
+
if (!fromExplicitSource && effectiveFallbackAuto) {
|
|
2526
|
+
const cap = this.lastResortCap();
|
|
2527
|
+
if (cap > 0) {
|
|
2528
|
+
const usedKeys = new Set(
|
|
2529
|
+
candidates.map((c) => `${c.providerId}/${c.model}`)
|
|
2530
|
+
);
|
|
2531
|
+
const lastResort = this.smartDefault(current, Number.POSITIVE_INFINITY).filter((c) => !usedKeys.has(`${c.providerId}/${c.model}`)).slice(0, cap);
|
|
2532
|
+
candidates.push(...lastResort);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
return dedupeChain(candidates, current);
|
|
2452
2537
|
}
|
|
2453
2538
|
// ── Provider availability (read-only) ──────────────────────────────────
|
|
2454
2539
|
checkProvider(providerId) {
|
|
@@ -2572,7 +2657,20 @@ var FallbackProfileManager = class {
|
|
|
2572
2657
|
);
|
|
2573
2658
|
}
|
|
2574
2659
|
};
|
|
2660
|
+
var MAX_LAST_RESORT_CANDIDATES = 12;
|
|
2575
2661
|
var FREEZER_EMPTY = Object.freeze([]);
|
|
2662
|
+
function dedupeChain(entries, current) {
|
|
2663
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2664
|
+
const currentKey = `${current.providerId}/${current.model}`;
|
|
2665
|
+
return Object.freeze(
|
|
2666
|
+
entries.filter((entry) => {
|
|
2667
|
+
const key = `${entry.providerId}/${entry.model}`;
|
|
2668
|
+
if (key === currentKey || seen.has(key)) return false;
|
|
2669
|
+
seen.add(key);
|
|
2670
|
+
return true;
|
|
2671
|
+
})
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2576
2674
|
|
|
2577
2675
|
// src/plugins/auto-review-plugin.ts
|
|
2578
2676
|
init_error();
|
|
@@ -6371,7 +6469,9 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
|
6371
6469
|
const agentRange = dayFilter("day");
|
|
6372
6470
|
const uniqueAgents = this.db.prepare(`SELECT COUNT(DISTINCT agent_id) n FROM agent_daily${agentRange.where}`).get(...agentRange.params).n;
|
|
6373
6471
|
const requestRange = dayFilter("day");
|
|
6374
|
-
const logicalRequests = this.db.prepare(
|
|
6472
|
+
const logicalRequests = this.db.prepare(
|
|
6473
|
+
`SELECT COUNT(DISTINCT logical_request_id) n FROM logical_request_daily${requestRange.where}`
|
|
6474
|
+
).get(...requestRange.params).n;
|
|
6375
6475
|
const fileRange = dayFilter("day");
|
|
6376
6476
|
const uniqueFiles = this.db.prepare(`SELECT COUNT(DISTINCT path_key) n FROM file_seen_daily${fileRange.where}`).get(...fileRange.params).n;
|
|
6377
6477
|
const costRange = dayFilter("day");
|
|
@@ -6680,10 +6780,13 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
|
6680
6780
|
ON CONFLICT(day, family) DO UPDATE SET count = count + 1, failure_count = failure_count + excluded.failure_count`
|
|
6681
6781
|
).run(day, family, failed);
|
|
6682
6782
|
if (failed) bump("failures = failures + 1");
|
|
6683
|
-
if (event.outcome === "cancelled" || event.outcome === "abandoned")
|
|
6783
|
+
if (event.outcome === "cancelled" || event.outcome === "abandoned")
|
|
6784
|
+
bump("cancellations = cancellations + 1");
|
|
6684
6785
|
if (family === "agent") bump("agent_events = agent_events + 1");
|
|
6685
6786
|
if (event.correlation.logicalRequestId) {
|
|
6686
|
-
this.db.prepare(
|
|
6787
|
+
this.db.prepare(
|
|
6788
|
+
"INSERT OR IGNORE INTO logical_request_daily (day, logical_request_id) VALUES (?, ?)"
|
|
6789
|
+
).run(day, event.correlation.logicalRequestId);
|
|
6687
6790
|
}
|
|
6688
6791
|
if (event.scope.agentId) {
|
|
6689
6792
|
this.db.prepare("INSERT OR IGNORE INTO agent_daily (day, agent_id) VALUES (?, ?)").run(day, event.scope.agentId);
|
|
@@ -6703,7 +6806,8 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
|
|
|
6703
6806
|
durationCount
|
|
6704
6807
|
);
|
|
6705
6808
|
} else if (type === "process.started") bump("processes = processes + 1");
|
|
6706
|
-
else if (type === "process.completed" && event.outcome === "failure")
|
|
6809
|
+
else if (type === "process.completed" && event.outcome === "failure")
|
|
6810
|
+
bump("failed_processes = failed_processes + 1");
|
|
6707
6811
|
if (event.resource?.kind === "file" || type.startsWith("file.")) {
|
|
6708
6812
|
bump("file_events_all = file_events_all + 1");
|
|
6709
6813
|
if (event.resource?.path) {
|
package/dist/registry/index.js
CHANGED
|
@@ -759,6 +759,17 @@ var ToolRegistry = class _ToolRegistry {
|
|
|
759
759
|
this._providerListSnapshotVersion = this._version;
|
|
760
760
|
return arr;
|
|
761
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Whether an enabled catalog tool is part of the direct provider surface.
|
|
764
|
+
*
|
|
765
|
+
* This is intentionally different from `get()`/`list()`: a tool can remain
|
|
766
|
+
* executable through a lazy gateway while its schema is omitted from the
|
|
767
|
+
* next provider request. Disabled and unknown tools always return false.
|
|
768
|
+
*/
|
|
769
|
+
isExposedToProvider(name) {
|
|
770
|
+
if (this._disabled.has(name) || !this.tools.has(name)) return false;
|
|
771
|
+
return this._providerToolNames?.has(name) ?? true;
|
|
772
|
+
}
|
|
762
773
|
/**
|
|
763
774
|
* Group tools by their `category` field. Tools without a category
|
|
764
775
|
* are placed under the key `""` (empty string). Returns a Map of
|
|
@@ -149,6 +149,14 @@ export declare class ToolRegistry {
|
|
|
149
149
|
exposeToProvider(names: string | readonly string[]): void;
|
|
150
150
|
/** Tools serialized into provider requests and described by the system prompt. */
|
|
151
151
|
listForProvider(): Tool[];
|
|
152
|
+
/**
|
|
153
|
+
* Whether an enabled catalog tool is part of the direct provider surface.
|
|
154
|
+
*
|
|
155
|
+
* This is intentionally different from `get()`/`list()`: a tool can remain
|
|
156
|
+
* executable through a lazy gateway while its schema is omitted from the
|
|
157
|
+
* next provider request. Disabled and unknown tools always return false.
|
|
158
|
+
*/
|
|
159
|
+
isExposedToProvider(name: string): boolean;
|
|
152
160
|
/**
|
|
153
161
|
* Group tools by their `category` field. Tools without a category
|
|
154
162
|
* are placed under the key `""` (empty string). Returns a Map of
|
package/dist/replay/hash.d.ts
CHANGED
|
@@ -17,6 +17,15 @@ import type { Request } from '../types/provider.js';
|
|
|
17
17
|
* sampling knobs (`temperature`, `topP`, `stopSequences`,
|
|
18
18
|
* `toolChoice`). Anything else on the `Request` (metadata,
|
|
19
19
|
* future extensions) is ignored so replay stays forward-compat.
|
|
20
|
+
* - The same rule applies *inside* each message. `Request.messages`
|
|
21
|
+
* is the live `ctx.messages` array, whose entries carry local
|
|
22
|
+
* bookkeeping the provider adapters explicitly drop: `ts` (wall
|
|
23
|
+
* clock at the moment the turn was created), `_estTokens` (a
|
|
24
|
+
* mutation-time cache) and `origin`. Hashing those made every
|
|
25
|
+
* hash a function of when the run happened, so a recorded
|
|
26
|
+
* response could never be found again and `mode: 'replay'` threw
|
|
27
|
+
* on the first call it was asked to serve. See
|
|
28
|
+
* {@link semanticMessage}.
|
|
20
29
|
* - We serialize to JSON. The `ContentBlock` and `Message` shapes
|
|
21
30
|
* are pure data; this works as long as no `undefined` values
|
|
22
31
|
* sneak in (those get dropped by `JSON.stringify`, which is
|
package/dist/replay/index.js
CHANGED
|
@@ -15,11 +15,15 @@ function sortKeys(value) {
|
|
|
15
15
|
}
|
|
16
16
|
return value;
|
|
17
17
|
}
|
|
18
|
+
function semanticMessage(message) {
|
|
19
|
+
const { ts: _ts, _estTokens: _estimate, origin: _origin, ...semantic } = message;
|
|
20
|
+
return semantic;
|
|
21
|
+
}
|
|
18
22
|
function hashRequest(request) {
|
|
19
23
|
const payload = {
|
|
20
24
|
model: request.model,
|
|
21
25
|
system: request.system,
|
|
22
|
-
messages: request.messages,
|
|
26
|
+
messages: request.messages.map(semanticMessage),
|
|
23
27
|
tools: request.tools,
|
|
24
28
|
maxTokens: request.maxTokens,
|
|
25
29
|
temperature: request.temperature,
|
|
@@ -40,16 +44,22 @@ var ReplayProviderRunner = class {
|
|
|
40
44
|
}
|
|
41
45
|
inner;
|
|
42
46
|
opts;
|
|
47
|
+
/** Resolve the target session for this call. See {@link ReplayProviderRunnerOptions.sessionId}. */
|
|
48
|
+
resolveSessionId() {
|
|
49
|
+
const { sessionId } = this.opts;
|
|
50
|
+
return typeof sessionId === "function" ? sessionId() : sessionId;
|
|
51
|
+
}
|
|
43
52
|
async run(runOpts) {
|
|
44
53
|
const hash = hashRequest(runOpts.request);
|
|
45
|
-
const
|
|
54
|
+
const sessionId = this.resolveSessionId();
|
|
55
|
+
const cached = await this.opts.log.lookup(sessionId, hash);
|
|
46
56
|
if (this.opts.mode === "replay") {
|
|
47
57
|
if (!cached) {
|
|
48
58
|
this.opts.logger?.warn?.(
|
|
49
59
|
`replay: no recorded response for hash ${hash} (model ${runOpts.request.model})`
|
|
50
60
|
);
|
|
51
61
|
throw new Error(
|
|
52
|
-
`ReplayProviderRunner: no recorded response for hash ${hash} in session ${
|
|
62
|
+
`ReplayProviderRunner: no recorded response for hash ${hash} in session ${sessionId}. Either the request changed since recording, or this session has no replay log.`
|
|
53
63
|
);
|
|
54
64
|
}
|
|
55
65
|
this.opts.logger?.debug?.(
|
|
@@ -65,7 +75,7 @@ var ReplayProviderRunner = class {
|
|
|
65
75
|
}
|
|
66
76
|
const response = await this.inner.run(runOpts);
|
|
67
77
|
await this.opts.log.record({
|
|
68
|
-
sessionId
|
|
78
|
+
sessionId,
|
|
69
79
|
request: runOpts.request,
|
|
70
80
|
response
|
|
71
81
|
});
|
|
@@ -26,11 +26,39 @@ import type { Response } from '../types/provider.js';
|
|
|
26
26
|
* a particular request is seen, it's recorded; the next time
|
|
27
27
|
* the same request is made, the recorded response is served.
|
|
28
28
|
* This is the cheapest path to a deterministic test harness.
|
|
29
|
+
*
|
|
30
|
+
* ## What a hit requires
|
|
31
|
+
*
|
|
32
|
+
* The lookup key is `hashRequest`, which covers everything the provider is
|
|
33
|
+
* actually sent — including `system`. The agent loop appends volatile blocks
|
|
34
|
+
* to the system prompt on every request (the continuity ledger, the live
|
|
35
|
+
* next-steps gate, memory evidence; see `agent-response.ts`), and those blocks
|
|
36
|
+
* describe the state of *this* run. A re-run whose ledger or retrieved memory
|
|
37
|
+
* differs therefore produces a different request, and correctly misses: the
|
|
38
|
+
* recorded response answered a different prompt.
|
|
39
|
+
*
|
|
40
|
+
* So `mode: 'replay'` reproduces a run faithfully only when the loop is driven
|
|
41
|
+
* back through the same states — which is what a test harness does, and what a
|
|
42
|
+
* fresh interactive session does not. In an interactive session, prefer
|
|
43
|
+
* `'record'` (build the log) and use `'replay'` from a harness that pins the
|
|
44
|
+
* volatile inputs. `hashRequest` itself is stable across time: local message
|
|
45
|
+
* bookkeeping (`ts`, `_estTokens`, `origin`) is stripped before hashing.
|
|
29
46
|
*/
|
|
30
47
|
export type ReplayMode = 'record' | 'replay' | 'auto';
|
|
31
48
|
export interface ReplayProviderRunnerOptions {
|
|
32
49
|
log: ReplayLogStore;
|
|
33
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Which session's log to read and write.
|
|
52
|
+
*
|
|
53
|
+
* Accepts a resolver because recording is wired before the session exists:
|
|
54
|
+
* the boot path binds this runner while the session writer is still being
|
|
55
|
+
* created, and the user may then swap sessions (`/resume`, new session)
|
|
56
|
+
* without the runner being rebuilt. A frozen string there meant the log was
|
|
57
|
+
* filed under a synthetic id with no session behind it. A resolver keeps the
|
|
58
|
+
* log next to the transcript it belongs to, for the session that is actually
|
|
59
|
+
* live at the time of the call.
|
|
60
|
+
*/
|
|
61
|
+
sessionId: string | (() => string);
|
|
34
62
|
mode: ReplayMode;
|
|
35
63
|
/**
|
|
36
64
|
* Optional logger — receives a debug line on every replay hit
|
|
@@ -48,6 +76,8 @@ export declare class ReplayProviderRunner implements ProviderRunner {
|
|
|
48
76
|
private readonly inner;
|
|
49
77
|
private readonly opts;
|
|
50
78
|
constructor(inner: ProviderRunner, opts: ReplayProviderRunnerOptions);
|
|
79
|
+
/** Resolve the target session for this call. See {@link ReplayProviderRunnerOptions.sessionId}. */
|
|
80
|
+
private resolveSessionId;
|
|
51
81
|
run(runOpts: RunProviderOptions): Promise<Response>;
|
|
52
82
|
}
|
|
53
83
|
//# sourceMappingURL=replay-provider-runner.d.ts.map
|
package/dist/security/index.js
CHANGED
|
@@ -1298,11 +1298,13 @@ function windowsAccountName() {
|
|
|
1298
1298
|
import { realpath } from "node:fs/promises";
|
|
1299
1299
|
import * as path2 from "node:path";
|
|
1300
1300
|
import {
|
|
1301
|
+
evaluateContractGraphReadiness,
|
|
1301
1302
|
evaluateKanbanBoundaryOpaque,
|
|
1302
1303
|
evaluateKanbanBoundaryPath,
|
|
1303
1304
|
readBoard,
|
|
1304
1305
|
resolveKanbanBoundaryLayers
|
|
1305
1306
|
} from "@wrongstack/kanban";
|
|
1307
|
+
var GOVERNANCE_CONTROL_TOOLS = /* @__PURE__ */ new Set(["kanban", "plan", "task", "todo"]);
|
|
1306
1308
|
var PATH_KEYS2 = /* @__PURE__ */ new Set([
|
|
1307
1309
|
"path",
|
|
1308
1310
|
"paths",
|
|
@@ -1327,12 +1329,62 @@ var TOOL_PATH_KEYS = {
|
|
|
1327
1329
|
// filesystem target in language_info. Keep ambiguous names tool-specific.
|
|
1328
1330
|
language_info: ["target"]
|
|
1329
1331
|
};
|
|
1330
|
-
async function evaluateToolKanbanBoundary(tool, input, ctx) {
|
|
1332
|
+
async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
1333
|
+
if (tool.name === "kanban") return { decision: "allow" };
|
|
1331
1334
|
const identity = resolveKanbanIdentity(ctx);
|
|
1332
|
-
|
|
1335
|
+
const governanceRequired = options.requireGovernance && isGovernedMutation(tool, input);
|
|
1336
|
+
if (!identity.boardId) {
|
|
1337
|
+
return governanceRequired ? {
|
|
1338
|
+
decision: "block",
|
|
1339
|
+
reason: "Kanban governance is mandatory before product mutation. Create a managed card, add its required details and executable acceptance criteria, then call kanban start_task to bind it to this run."
|
|
1340
|
+
} : { decision: "allow" };
|
|
1341
|
+
}
|
|
1333
1342
|
const board = await readBoard(identity.policyRoot ?? ctx.projectRoot, identity.boardId);
|
|
1334
|
-
if (!board)
|
|
1343
|
+
if (!board) {
|
|
1344
|
+
return governanceRequired ? {
|
|
1345
|
+
decision: "block",
|
|
1346
|
+
reason: `Active Kanban board not found: ${identity.boardId}. Recreate or select a valid managed card before mutation.`,
|
|
1347
|
+
boardId: identity.boardId,
|
|
1348
|
+
...identity.taskId ? { taskId: identity.taskId } : {}
|
|
1349
|
+
} : { decision: "allow" };
|
|
1350
|
+
}
|
|
1335
1351
|
const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
|
|
1352
|
+
if (governanceRequired && board.lifecycle?.mode === "managed") {
|
|
1353
|
+
if (!identity.taskId) {
|
|
1354
|
+
return {
|
|
1355
|
+
decision: "block",
|
|
1356
|
+
reason: "Kanban governance requires an active task, not only a board. Complete the card details and call kanban start_task before mutation.",
|
|
1357
|
+
boardId: board.id
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
if (!task) {
|
|
1361
|
+
return {
|
|
1362
|
+
decision: "block",
|
|
1363
|
+
reason: `Active Kanban task not found: ${identity.taskId}. Call kanban start_task with a valid card.`,
|
|
1364
|
+
boardId: board.id,
|
|
1365
|
+
taskId: identity.taskId
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
const readiness = evaluateContractGraphReadiness(board, task.id);
|
|
1369
|
+
if (!readiness.ready) {
|
|
1370
|
+
return {
|
|
1371
|
+
decision: "block",
|
|
1372
|
+
reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
|
|
1373
|
+
boardId: board.id,
|
|
1374
|
+
taskId: task.id
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
|
|
1378
|
+
const lifecycleStage = task.lifecycle?.currentStage ?? "missing";
|
|
1379
|
+
const assignmentStatus = task.assignment?.status ?? "missing";
|
|
1380
|
+
return {
|
|
1381
|
+
decision: "block",
|
|
1382
|
+
reason: `Active card must be in Running with a live assignment before product mutation (lifecycle: ${lifecycleStage}; assignment: ${assignmentStatus}). Call kanban start_task after completing the required card details.`,
|
|
1383
|
+
boardId: board.id,
|
|
1384
|
+
taskId: task.id
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1336
1388
|
if (identity.leaseId && task?.assignment) {
|
|
1337
1389
|
const caps2 = tool.capabilities ?? [];
|
|
1338
1390
|
const isWrite = caps2.includes("fs.write") || caps2.includes("fs.write.outside-project") || caps2.some((c) => c.startsWith("shell."));
|
|
@@ -1368,6 +1420,19 @@ async function evaluateToolKanbanBoundary(tool, input, ctx) {
|
|
|
1368
1420
|
...task ? { taskId: task.id } : {}
|
|
1369
1421
|
};
|
|
1370
1422
|
}
|
|
1423
|
+
function isGovernedMutation(tool, input) {
|
|
1424
|
+
if (!tool.mutating || GOVERNANCE_CONTROL_TOOLS.has(tool.name)) return false;
|
|
1425
|
+
if (tool.capabilities?.includes("tool.meta")) return false;
|
|
1426
|
+
if (tool.name === "git") {
|
|
1427
|
+
const command = input["command"];
|
|
1428
|
+
if (command === "status" || command === "log" || command === "diff") return false;
|
|
1429
|
+
if (command === "worktree" && input["worktreeAction"] === "list") return false;
|
|
1430
|
+
}
|
|
1431
|
+
const capabilities = tool.capabilities ?? [];
|
|
1432
|
+
return capabilities.some(
|
|
1433
|
+
(capability) => capability === "fs.write" || capability === "fs.write.outside-project" || capability === "package.install" || capability === "tool.mutate.any" || capability.startsWith("shell.") || capability === "net.outbound"
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1371
1436
|
function resolveKanbanIdentity(ctx) {
|
|
1372
1437
|
const metaKanban = ctx.meta["kanban"];
|
|
1373
1438
|
const record = metaKanban && typeof metaKanban === "object" ? metaKanban : void 0;
|
|
@@ -1685,6 +1750,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
|
|
|
1685
1750
|
const obj = input;
|
|
1686
1751
|
if (subjectKey) {
|
|
1687
1752
|
const value = obj[subjectKey];
|
|
1753
|
+
if (Array.isArray(value)) {
|
|
1754
|
+
return escapeGlobSubject(JSON.stringify(value));
|
|
1755
|
+
}
|
|
1688
1756
|
if (typeof value === "string") {
|
|
1689
1757
|
if (isPathSubjectKey(subjectKey)) {
|
|
1690
1758
|
const normalized = normalizePathSubject(value);
|
|
@@ -3541,23 +3609,21 @@ function unwrapDataKey(buf, keyFile) {
|
|
|
3541
3609
|
});
|
|
3542
3610
|
}
|
|
3543
3611
|
}
|
|
3544
|
-
function
|
|
3545
|
-
if (process.platform === "win32") return;
|
|
3612
|
+
function keyFileNeedsHardening(keyFile, opts) {
|
|
3613
|
+
if (process.platform === "win32") return false;
|
|
3546
3614
|
const warn = opts?.warn ?? ((msg) => console.warn(msg));
|
|
3547
3615
|
try {
|
|
3548
3616
|
const stat = fs2.statSync(keyFile);
|
|
3549
3617
|
const actualMode = stat.mode & 511;
|
|
3550
3618
|
if (actualMode !== KEY_FILE_MODE) {
|
|
3551
|
-
void restrictFilePermissions(keyFile, {
|
|
3552
|
-
label: "secret-vault",
|
|
3553
|
-
warn
|
|
3554
|
-
}).catch(() => void 0);
|
|
3555
3619
|
warn(
|
|
3556
3620
|
`Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Hardening\u2026`
|
|
3557
3621
|
);
|
|
3622
|
+
return true;
|
|
3558
3623
|
}
|
|
3559
3624
|
} catch {
|
|
3560
3625
|
}
|
|
3626
|
+
return false;
|
|
3561
3627
|
}
|
|
3562
3628
|
function writeKeyFileAtomicSync(keyFile, content) {
|
|
3563
3629
|
const tmp = `${keyFile}.${randomBytes2(4).toString("hex")}.tmp`;
|
|
@@ -3623,6 +3689,14 @@ var DefaultSecretVault = class {
|
|
|
3623
3689
|
}).catch(() => void 0);
|
|
3624
3690
|
this.pendingHardening.push(p);
|
|
3625
3691
|
}
|
|
3692
|
+
/** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
|
|
3693
|
+
checkKeyFilePermissions() {
|
|
3694
|
+
if (keyFileNeedsHardening(this.keyFile, {
|
|
3695
|
+
warn: (msg) => this.logWarn(msg)
|
|
3696
|
+
})) {
|
|
3697
|
+
this.scheduleKeyHardening();
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3626
3700
|
/** Flush all pending key-file hardening promises. */
|
|
3627
3701
|
flushHardening() {
|
|
3628
3702
|
if (this.pendingHardening.length === 0) return Promise.resolve();
|
|
@@ -3710,7 +3784,7 @@ var DefaultSecretVault = class {
|
|
|
3710
3784
|
writeKeyFileAtomicSync(this.keyFile, keyFileBuf);
|
|
3711
3785
|
}
|
|
3712
3786
|
this.scheduleKeyHardening();
|
|
3713
|
-
|
|
3787
|
+
this.checkKeyFilePermissions();
|
|
3714
3788
|
this.key = newKey;
|
|
3715
3789
|
this._keyVersion = newVersion;
|
|
3716
3790
|
return { oldVersion, newVersion };
|
|
@@ -3726,12 +3800,9 @@ var DefaultSecretVault = class {
|
|
|
3726
3800
|
const passphrase = getVaultPassphrase();
|
|
3727
3801
|
if (!passphrase || !this.key) return;
|
|
3728
3802
|
try {
|
|
3729
|
-
writeKeyFileAtomicSync(
|
|
3730
|
-
this.keyFile,
|
|
3731
|
-
wrapDataKey(this.key, this._keyVersion, passphrase)
|
|
3732
|
-
);
|
|
3803
|
+
writeKeyFileAtomicSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase));
|
|
3733
3804
|
this.scheduleKeyHardening();
|
|
3734
|
-
|
|
3805
|
+
this.checkKeyFilePermissions();
|
|
3735
3806
|
} catch {
|
|
3736
3807
|
}
|
|
3737
3808
|
}
|
|
@@ -3743,13 +3814,13 @@ var DefaultSecretVault = class {
|
|
|
3743
3814
|
const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
|
|
3744
3815
|
this.key = key2;
|
|
3745
3816
|
this._keyVersion = version;
|
|
3746
|
-
|
|
3817
|
+
this.checkKeyFilePermissions();
|
|
3747
3818
|
return this.key;
|
|
3748
3819
|
}
|
|
3749
3820
|
if (buf.length === KEY_BYTES) {
|
|
3750
3821
|
this.key = buf;
|
|
3751
3822
|
this._keyVersion = 1;
|
|
3752
|
-
|
|
3823
|
+
this.checkKeyFilePermissions();
|
|
3753
3824
|
this.migrateToWrappedIfPassphrase();
|
|
3754
3825
|
return this.key;
|
|
3755
3826
|
}
|
|
@@ -3773,7 +3844,7 @@ var DefaultSecretVault = class {
|
|
|
3773
3844
|
}
|
|
3774
3845
|
this.key = Buffer.from(key2);
|
|
3775
3846
|
this._keyVersion = version;
|
|
3776
|
-
|
|
3847
|
+
this.checkKeyFilePermissions();
|
|
3777
3848
|
this.migrateToWrappedIfPassphrase();
|
|
3778
3849
|
return this.key;
|
|
3779
3850
|
}
|
|
@@ -3800,13 +3871,13 @@ var DefaultSecretVault = class {
|
|
|
3800
3871
|
const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
|
|
3801
3872
|
this.key = winnerKey;
|
|
3802
3873
|
this._keyVersion = version;
|
|
3803
|
-
|
|
3874
|
+
this.checkKeyFilePermissions();
|
|
3804
3875
|
return this.key;
|
|
3805
3876
|
}
|
|
3806
3877
|
if (buf.length === KEY_BYTES) {
|
|
3807
3878
|
this.key = buf;
|
|
3808
3879
|
this._keyVersion = 1;
|
|
3809
|
-
|
|
3880
|
+
this.checkKeyFilePermissions();
|
|
3810
3881
|
return this.key;
|
|
3811
3882
|
}
|
|
3812
3883
|
if (buf.length === VERSIONED_KEY_FILE_SIZE) {
|
|
@@ -3822,7 +3893,7 @@ var DefaultSecretVault = class {
|
|
|
3822
3893
|
const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
|
|
3823
3894
|
this.key = Buffer.from(winnerKey);
|
|
3824
3895
|
this._keyVersion = version;
|
|
3825
|
-
|
|
3896
|
+
this.checkKeyFilePermissions();
|
|
3826
3897
|
return this.key;
|
|
3827
3898
|
}
|
|
3828
3899
|
throw new ConfigError({
|
|
@@ -5,6 +5,10 @@ export interface ToolKanbanBoundaryEvaluation extends KanbanBoundaryEvaluation {
|
|
|
5
5
|
boardId?: string | undefined;
|
|
6
6
|
taskId?: string | undefined;
|
|
7
7
|
}
|
|
8
|
+
export interface ToolKanbanGovernanceOptions {
|
|
9
|
+
/** Require every product mutation to run inside a ready, running Kanban card. */
|
|
10
|
+
requireGovernance?: boolean | undefined;
|
|
11
|
+
}
|
|
8
12
|
/** Resolve the live board/task policy and gate one tool invocation. */
|
|
9
|
-
export declare function evaluateToolKanbanBoundary(tool: Tool, input: Record<string, unknown>, ctx: Context): Promise<ToolKanbanBoundaryEvaluation>;
|
|
13
|
+
export declare function evaluateToolKanbanBoundary(tool: Tool, input: Record<string, unknown>, ctx: Context, options?: ToolKanbanGovernanceOptions): Promise<ToolKanbanBoundaryEvaluation>;
|
|
10
14
|
//# sourceMappingURL=kanban-boundary.d.ts.map
|
|
@@ -46,6 +46,8 @@ export declare class DefaultSecretVault implements RotatableSecretVault {
|
|
|
46
46
|
* can never become an unhandled one.
|
|
47
47
|
*/
|
|
48
48
|
private scheduleKeyHardening;
|
|
49
|
+
/** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
|
|
50
|
+
private checkKeyFilePermissions;
|
|
49
51
|
/** Flush all pending key-file hardening promises. */
|
|
50
52
|
flushHardening(): Promise<void>;
|
|
51
53
|
/** Current key version. Starts at 1; incremented by rotateKey(). */
|