@wrongstack/core 0.306.2 → 0.306.3
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/dist/coordination/index.js +48 -46
- package/dist/coordination/mailbox-project-server.js +38 -16
- package/dist/coordination/sqlite-mailbox.d.ts +1 -0
- package/dist/defaults/index.js +52 -48
- package/dist/execution/index.js +4 -2
- package/dist/index.js +54 -50
- package/dist/security/index.js +2 -1
- package/dist/security/kanban-boundary.d.ts +3 -1
- package/dist/storage/index.js +50 -48
- package/dist/types/blocks.d.ts +9 -0
- package/instructions/agents/code-reviewer.md +3 -0
- package/instructions/coordination/subagent-baseline.md +10 -1
- package/instructions/system-lite.md +8 -5
- package/instructions/system-pro.md +26 -0
- package/instructions/system.md +21 -0
- package/package.json +3 -3
|
@@ -14584,6 +14584,44 @@ function userInputTitle(content) {
|
|
|
14584
14584
|
return sessionContentPreview(content, 60);
|
|
14585
14585
|
}
|
|
14586
14586
|
|
|
14587
|
+
// src/storage/session-writer-scrubber.ts
|
|
14588
|
+
function scrubSessionWriterEvent(event, secretScrubber) {
|
|
14589
|
+
const persistMessage = (message) => {
|
|
14590
|
+
const { _estTokens: _ignored, ...persisted } = message;
|
|
14591
|
+
return {
|
|
14592
|
+
...persisted,
|
|
14593
|
+
content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
|
|
14594
|
+
};
|
|
14595
|
+
};
|
|
14596
|
+
if (event.type === "context_snapshot" || event.type === "messages_replaced") {
|
|
14597
|
+
return { ...event, messages: event.messages.map(persistMessage) };
|
|
14598
|
+
}
|
|
14599
|
+
if (event.type === "message_appended" || event.type === "message_updated") {
|
|
14600
|
+
return { ...event, message: persistMessage(event.message) };
|
|
14601
|
+
}
|
|
14602
|
+
if (!secretScrubber) return event;
|
|
14603
|
+
if (event.type === "user_input") {
|
|
14604
|
+
return {
|
|
14605
|
+
...event,
|
|
14606
|
+
content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
|
|
14607
|
+
};
|
|
14608
|
+
}
|
|
14609
|
+
if (event.type === "llm_response") {
|
|
14610
|
+
return { ...event, content: secretScrubber.scrubObject(event.content) };
|
|
14611
|
+
}
|
|
14612
|
+
if (event.type === "file_snapshot") {
|
|
14613
|
+
return {
|
|
14614
|
+
...event,
|
|
14615
|
+
files: event.files.map((f) => ({
|
|
14616
|
+
...f,
|
|
14617
|
+
before: f.before !== null ? secretScrubber.scrub(f.before) : null,
|
|
14618
|
+
after: f.after !== null ? secretScrubber.scrub(f.after) : null
|
|
14619
|
+
}))
|
|
14620
|
+
};
|
|
14621
|
+
}
|
|
14622
|
+
return event;
|
|
14623
|
+
}
|
|
14624
|
+
|
|
14587
14625
|
// src/storage/session-writer-truncate.ts
|
|
14588
14626
|
import * as fsp6 from "node:fs/promises";
|
|
14589
14627
|
var CHUNK_SIZE = 65536;
|
|
@@ -14728,45 +14766,11 @@ async function rewriteSessionToCheckpoint(filePath, checkpointByteOffset) {
|
|
|
14728
14766
|
}
|
|
14729
14767
|
}
|
|
14730
14768
|
|
|
14731
|
-
// src/storage/session-writer-scrubber.ts
|
|
14732
|
-
function scrubSessionWriterEvent(event, secretScrubber) {
|
|
14733
|
-
const persistMessage = (message) => {
|
|
14734
|
-
const { _estTokens: _ignored, ...persisted } = message;
|
|
14735
|
-
return {
|
|
14736
|
-
...persisted,
|
|
14737
|
-
content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
|
|
14738
|
-
};
|
|
14739
|
-
};
|
|
14740
|
-
if (event.type === "context_snapshot" || event.type === "messages_replaced") {
|
|
14741
|
-
return { ...event, messages: event.messages.map(persistMessage) };
|
|
14742
|
-
}
|
|
14743
|
-
if (event.type === "message_appended" || event.type === "message_updated") {
|
|
14744
|
-
return { ...event, message: persistMessage(event.message) };
|
|
14745
|
-
}
|
|
14746
|
-
if (!secretScrubber) return event;
|
|
14747
|
-
if (event.type === "user_input") {
|
|
14748
|
-
return {
|
|
14749
|
-
...event,
|
|
14750
|
-
content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
|
|
14751
|
-
};
|
|
14752
|
-
}
|
|
14753
|
-
if (event.type === "llm_response") {
|
|
14754
|
-
return { ...event, content: secretScrubber.scrubObject(event.content) };
|
|
14755
|
-
}
|
|
14756
|
-
if (event.type === "file_snapshot") {
|
|
14757
|
-
return {
|
|
14758
|
-
...event,
|
|
14759
|
-
files: event.files.map((f) => ({
|
|
14760
|
-
...f,
|
|
14761
|
-
before: f.before !== null ? secretScrubber.scrub(f.before) : null,
|
|
14762
|
-
after: f.after !== null ? secretScrubber.scrub(f.after) : null
|
|
14763
|
-
}))
|
|
14764
|
-
};
|
|
14765
|
-
}
|
|
14766
|
-
return event;
|
|
14767
|
-
}
|
|
14768
|
-
|
|
14769
14769
|
// src/storage/file-session-writer.ts
|
|
14770
|
+
function isClosedHandleError(err) {
|
|
14771
|
+
const code = err?.code;
|
|
14772
|
+
return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
|
|
14773
|
+
}
|
|
14770
14774
|
var FileSessionWriter = class _FileSessionWriter {
|
|
14771
14775
|
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
14772
14776
|
this.id = id;
|
|
@@ -14938,8 +14942,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
14938
14942
|
try {
|
|
14939
14943
|
return await this.handle.appendFile(data, "utf8");
|
|
14940
14944
|
} catch (err) {
|
|
14941
|
-
|
|
14942
|
-
if (nodeErr?.code === "EBADF") {
|
|
14945
|
+
if (isClosedHandleError(err)) {
|
|
14943
14946
|
this.handle = await fsp7.open(this.filePath, "a", 384);
|
|
14944
14947
|
return await this.handle.appendFile(data, "utf8");
|
|
14945
14948
|
}
|
|
@@ -14979,8 +14982,8 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
14979
14982
|
bufferSynchronousEvent(event) {
|
|
14980
14983
|
if (this.closed) return;
|
|
14981
14984
|
void this.ensureInit();
|
|
14982
|
-
this.
|
|
14983
|
-
|
|
14985
|
+
const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
|
|
14986
|
+
this.observeForSummary(appendEvent);
|
|
14984
14987
|
try {
|
|
14985
14988
|
this._onAppend?.(appendEvent);
|
|
14986
14989
|
} catch {
|
|
@@ -15133,8 +15136,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
15133
15136
|
try {
|
|
15134
15137
|
await this.handle.datasync();
|
|
15135
15138
|
} catch (err) {
|
|
15136
|
-
|
|
15137
|
-
if (nodeErr?.code === "EBADF") {
|
|
15139
|
+
if (isClosedHandleError(err)) {
|
|
15138
15140
|
this.handle = await fsp7.open(this.filePath, "a", 384);
|
|
15139
15141
|
return;
|
|
15140
15142
|
}
|
|
@@ -15371,6 +15373,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
15371
15373
|
return this.closePromise;
|
|
15372
15374
|
}
|
|
15373
15375
|
async doClose() {
|
|
15376
|
+
await this.ensureInit();
|
|
15374
15377
|
if (this.pendingFileSnapshots.length > 0) {
|
|
15375
15378
|
await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
|
|
15376
15379
|
this.pendingFileSnapshots = [];
|
|
@@ -15386,8 +15389,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
15386
15389
|
try {
|
|
15387
15390
|
await this.handle.datasync();
|
|
15388
15391
|
} catch (err) {
|
|
15389
|
-
|
|
15390
|
-
if (nodeErr?.code !== "EBADF") throw err;
|
|
15392
|
+
if (!isClosedHandleError(err)) throw err;
|
|
15391
15393
|
}
|
|
15392
15394
|
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15393
15395
|
const observedActivityMs = Date.parse(this.lastActivityAt);
|
|
@@ -1972,16 +1972,18 @@ function persistReceipt(db, messageId, state) {
|
|
|
1972
1972
|
}
|
|
1973
1973
|
function materializeMessageRows(db, rows) {
|
|
1974
1974
|
if (rows.length === 0) return [];
|
|
1975
|
-
const
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1975
|
+
const receiptRows = [];
|
|
1976
|
+
for (const ids of chunk(
|
|
1977
|
+
rows.map((row) => row.id),
|
|
1978
|
+
400
|
|
1979
|
+
)) {
|
|
1980
|
+
const receiptSql = `
|
|
1981
1981
|
SELECT message_id, actor_id, read_at, completed_at, completed_by, outcome
|
|
1982
1982
|
FROM message_receipts
|
|
1983
|
+
WHERE message_id IN (${ids.map(() => "?").join(", ")})
|
|
1983
1984
|
`;
|
|
1984
|
-
|
|
1985
|
+
receiptRows.push(...db.prepare(receiptSql).all(...ids));
|
|
1986
|
+
}
|
|
1985
1987
|
const receiptState = /* @__PURE__ */ new Map();
|
|
1986
1988
|
for (const row of receiptRows) {
|
|
1987
1989
|
const states = receiptState.get(row.message_id) ?? {};
|
|
@@ -2010,8 +2012,16 @@ function materializeMessageRows(db, rows) {
|
|
|
2010
2012
|
});
|
|
2011
2013
|
}
|
|
2012
2014
|
function deleteMessages(db, ids) {
|
|
2013
|
-
const
|
|
2014
|
-
|
|
2015
|
+
for (const chunkIds of chunk(ids, 400)) {
|
|
2016
|
+
db.prepare(`DELETE FROM messages WHERE id IN (${chunkIds.map(() => "?").join(", ")})`).run(
|
|
2017
|
+
...chunkIds
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
function* chunk(values, size) {
|
|
2022
|
+
for (let start = 0; start < values.length; start += size) {
|
|
2023
|
+
yield values.slice(start, start + size);
|
|
2024
|
+
}
|
|
2015
2025
|
}
|
|
2016
2026
|
function persistAgent(db, agent) {
|
|
2017
2027
|
db.prepare(`
|
|
@@ -2264,11 +2274,11 @@ function createMailboxParseState(raw) {
|
|
|
2264
2274
|
ingestMailboxChunk(state, raw);
|
|
2265
2275
|
return state;
|
|
2266
2276
|
}
|
|
2267
|
-
function ingestMailboxChunk(state,
|
|
2277
|
+
function ingestMailboxChunk(state, chunk2) {
|
|
2268
2278
|
const firstNewIndex = state.messages.length;
|
|
2269
2279
|
const ackRecords = [];
|
|
2270
2280
|
const staleExisting = /* @__PURE__ */ new Set();
|
|
2271
|
-
for (const line of
|
|
2281
|
+
for (const line of chunk2.split(LINE_SEPARATOR)) {
|
|
2272
2282
|
if (line.trim().length === 0) continue;
|
|
2273
2283
|
let parsed;
|
|
2274
2284
|
try {
|
|
@@ -2580,6 +2590,7 @@ var SqliteMailbox = class {
|
|
|
2580
2590
|
lastHeartbeat = /* @__PURE__ */ new Map();
|
|
2581
2591
|
lastClientHeartbeat = /* @__PURE__ */ new Map();
|
|
2582
2592
|
autoCompactTimer = null;
|
|
2593
|
+
autoCompactInFlight;
|
|
2583
2594
|
closed = false;
|
|
2584
2595
|
stmt(sql) {
|
|
2585
2596
|
return this.db.prepare(sql);
|
|
@@ -3023,10 +3034,14 @@ var SqliteMailbox = class {
|
|
|
3023
3034
|
* heartbeat from that id simply is not throttled and writes once more.
|
|
3024
3035
|
*/
|
|
3025
3036
|
pruneHeartbeats(map, nowMs) {
|
|
3026
|
-
if (map.size <= HEARTBEAT_TRACKING_MAX_ENTRIES) return;
|
|
3027
3037
|
for (const [id, at] of map) {
|
|
3028
3038
|
if (nowMs - at > HEARTBEAT_TRACKING_TTL_MS) map.delete(id);
|
|
3029
3039
|
}
|
|
3040
|
+
while (map.size > HEARTBEAT_TRACKING_MAX_ENTRIES) {
|
|
3041
|
+
const oldest = map.keys().next().value;
|
|
3042
|
+
if (oldest === void 0) break;
|
|
3043
|
+
map.delete(oldest);
|
|
3044
|
+
}
|
|
3030
3045
|
}
|
|
3031
3046
|
async deregisterAgent(agentId) {
|
|
3032
3047
|
this.stmt("DELETE FROM agents WHERE agent_id = ?").run(agentId);
|
|
@@ -3130,7 +3145,14 @@ var SqliteMailbox = class {
|
|
|
3130
3145
|
return purgeStale(this.compactionCtx(), options);
|
|
3131
3146
|
}
|
|
3132
3147
|
async autoCompact(options) {
|
|
3133
|
-
|
|
3148
|
+
if (this.autoCompactInFlight !== void 0) return this.autoCompactInFlight;
|
|
3149
|
+
const inFlight = autoCompact(this.compactionCtx(), options);
|
|
3150
|
+
this.autoCompactInFlight = inFlight;
|
|
3151
|
+
try {
|
|
3152
|
+
return await inFlight;
|
|
3153
|
+
} finally {
|
|
3154
|
+
if (this.autoCompactInFlight === inFlight) this.autoCompactInFlight = void 0;
|
|
3155
|
+
}
|
|
3134
3156
|
}
|
|
3135
3157
|
/** Bundle of store operations the retention sweeps drive. */
|
|
3136
3158
|
compactionCtx() {
|
|
@@ -3446,9 +3468,9 @@ function handleMessage(state, message) {
|
|
|
3446
3468
|
scheduleIdleStop();
|
|
3447
3469
|
});
|
|
3448
3470
|
}
|
|
3449
|
-
function onData(state,
|
|
3471
|
+
function onData(state, chunk2) {
|
|
3450
3472
|
state.lastSeenAt = Date.now();
|
|
3451
|
-
state.buffer +=
|
|
3473
|
+
state.buffer += chunk2;
|
|
3452
3474
|
while (true) {
|
|
3453
3475
|
const newline = state.buffer.indexOf("\n");
|
|
3454
3476
|
if (newline < 0) {
|
|
@@ -3544,7 +3566,7 @@ var server = net.createServer((socket) => {
|
|
|
3544
3566
|
void metadataWritten.then(() => {
|
|
3545
3567
|
if (!socket.destroyed) send(state, { type: "hello", ...serverInfo });
|
|
3546
3568
|
});
|
|
3547
|
-
socket.on("data", (
|
|
3569
|
+
socket.on("data", (chunk2) => onData(state, chunk2));
|
|
3548
3570
|
socket.on("error", () => {
|
|
3549
3571
|
});
|
|
3550
3572
|
socket.on("close", () => {
|
|
@@ -14,6 +14,7 @@ export declare class SqliteMailbox implements Mailbox {
|
|
|
14
14
|
private readonly lastHeartbeat;
|
|
15
15
|
private readonly lastClientHeartbeat;
|
|
16
16
|
private autoCompactTimer;
|
|
17
|
+
private autoCompactInFlight;
|
|
17
18
|
private closed;
|
|
18
19
|
constructor(projectDir: string, events?: EventBus, eventEmitter?: MailboxEventEmitter);
|
|
19
20
|
private stmt;
|
package/dist/defaults/index.js
CHANGED
|
@@ -14005,6 +14005,44 @@ function userInputTitle(content) {
|
|
|
14005
14005
|
return sessionContentPreview(content, 60);
|
|
14006
14006
|
}
|
|
14007
14007
|
|
|
14008
|
+
// src/storage/session-writer-scrubber.ts
|
|
14009
|
+
function scrubSessionWriterEvent(event, secretScrubber) {
|
|
14010
|
+
const persistMessage = (message) => {
|
|
14011
|
+
const { _estTokens: _ignored, ...persisted } = message;
|
|
14012
|
+
return {
|
|
14013
|
+
...persisted,
|
|
14014
|
+
content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
|
|
14015
|
+
};
|
|
14016
|
+
};
|
|
14017
|
+
if (event.type === "context_snapshot" || event.type === "messages_replaced") {
|
|
14018
|
+
return { ...event, messages: event.messages.map(persistMessage) };
|
|
14019
|
+
}
|
|
14020
|
+
if (event.type === "message_appended" || event.type === "message_updated") {
|
|
14021
|
+
return { ...event, message: persistMessage(event.message) };
|
|
14022
|
+
}
|
|
14023
|
+
if (!secretScrubber) return event;
|
|
14024
|
+
if (event.type === "user_input") {
|
|
14025
|
+
return {
|
|
14026
|
+
...event,
|
|
14027
|
+
content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
|
|
14028
|
+
};
|
|
14029
|
+
}
|
|
14030
|
+
if (event.type === "llm_response") {
|
|
14031
|
+
return { ...event, content: secretScrubber.scrubObject(event.content) };
|
|
14032
|
+
}
|
|
14033
|
+
if (event.type === "file_snapshot") {
|
|
14034
|
+
return {
|
|
14035
|
+
...event,
|
|
14036
|
+
files: event.files.map((f) => ({
|
|
14037
|
+
...f,
|
|
14038
|
+
before: f.before !== null ? secretScrubber.scrub(f.before) : null,
|
|
14039
|
+
after: f.after !== null ? secretScrubber.scrub(f.after) : null
|
|
14040
|
+
}))
|
|
14041
|
+
};
|
|
14042
|
+
}
|
|
14043
|
+
return event;
|
|
14044
|
+
}
|
|
14045
|
+
|
|
14008
14046
|
// src/storage/session-writer-truncate.ts
|
|
14009
14047
|
import * as fsp6 from "node:fs/promises";
|
|
14010
14048
|
var CHUNK_SIZE = 65536;
|
|
@@ -14149,45 +14187,11 @@ async function rewriteSessionToCheckpoint(filePath, checkpointByteOffset) {
|
|
|
14149
14187
|
}
|
|
14150
14188
|
}
|
|
14151
14189
|
|
|
14152
|
-
// src/storage/session-writer-scrubber.ts
|
|
14153
|
-
function scrubSessionWriterEvent(event, secretScrubber) {
|
|
14154
|
-
const persistMessage = (message) => {
|
|
14155
|
-
const { _estTokens: _ignored, ...persisted } = message;
|
|
14156
|
-
return {
|
|
14157
|
-
...persisted,
|
|
14158
|
-
content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
|
|
14159
|
-
};
|
|
14160
|
-
};
|
|
14161
|
-
if (event.type === "context_snapshot" || event.type === "messages_replaced") {
|
|
14162
|
-
return { ...event, messages: event.messages.map(persistMessage) };
|
|
14163
|
-
}
|
|
14164
|
-
if (event.type === "message_appended" || event.type === "message_updated") {
|
|
14165
|
-
return { ...event, message: persistMessage(event.message) };
|
|
14166
|
-
}
|
|
14167
|
-
if (!secretScrubber) return event;
|
|
14168
|
-
if (event.type === "user_input") {
|
|
14169
|
-
return {
|
|
14170
|
-
...event,
|
|
14171
|
-
content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
|
|
14172
|
-
};
|
|
14173
|
-
}
|
|
14174
|
-
if (event.type === "llm_response") {
|
|
14175
|
-
return { ...event, content: secretScrubber.scrubObject(event.content) };
|
|
14176
|
-
}
|
|
14177
|
-
if (event.type === "file_snapshot") {
|
|
14178
|
-
return {
|
|
14179
|
-
...event,
|
|
14180
|
-
files: event.files.map((f) => ({
|
|
14181
|
-
...f,
|
|
14182
|
-
before: f.before !== null ? secretScrubber.scrub(f.before) : null,
|
|
14183
|
-
after: f.after !== null ? secretScrubber.scrub(f.after) : null
|
|
14184
|
-
}))
|
|
14185
|
-
};
|
|
14186
|
-
}
|
|
14187
|
-
return event;
|
|
14188
|
-
}
|
|
14189
|
-
|
|
14190
14190
|
// src/storage/file-session-writer.ts
|
|
14191
|
+
function isClosedHandleError(err) {
|
|
14192
|
+
const code = err?.code;
|
|
14193
|
+
return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
|
|
14194
|
+
}
|
|
14191
14195
|
var FileSessionWriter = class _FileSessionWriter {
|
|
14192
14196
|
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
14193
14197
|
this.id = id;
|
|
@@ -14359,8 +14363,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
14359
14363
|
try {
|
|
14360
14364
|
return await this.handle.appendFile(data, "utf8");
|
|
14361
14365
|
} catch (err) {
|
|
14362
|
-
|
|
14363
|
-
if (nodeErr?.code === "EBADF") {
|
|
14366
|
+
if (isClosedHandleError(err)) {
|
|
14364
14367
|
this.handle = await fsp7.open(this.filePath, "a", 384);
|
|
14365
14368
|
return await this.handle.appendFile(data, "utf8");
|
|
14366
14369
|
}
|
|
@@ -14400,8 +14403,8 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
14400
14403
|
bufferSynchronousEvent(event) {
|
|
14401
14404
|
if (this.closed) return;
|
|
14402
14405
|
void this.ensureInit();
|
|
14403
|
-
this.
|
|
14404
|
-
|
|
14406
|
+
const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
|
|
14407
|
+
this.observeForSummary(appendEvent);
|
|
14405
14408
|
try {
|
|
14406
14409
|
this._onAppend?.(appendEvent);
|
|
14407
14410
|
} catch {
|
|
@@ -14554,8 +14557,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
14554
14557
|
try {
|
|
14555
14558
|
await this.handle.datasync();
|
|
14556
14559
|
} catch (err) {
|
|
14557
|
-
|
|
14558
|
-
if (nodeErr?.code === "EBADF") {
|
|
14560
|
+
if (isClosedHandleError(err)) {
|
|
14559
14561
|
this.handle = await fsp7.open(this.filePath, "a", 384);
|
|
14560
14562
|
return;
|
|
14561
14563
|
}
|
|
@@ -14792,6 +14794,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
14792
14794
|
return this.closePromise;
|
|
14793
14795
|
}
|
|
14794
14796
|
async doClose() {
|
|
14797
|
+
await this.ensureInit();
|
|
14795
14798
|
if (this.pendingFileSnapshots.length > 0) {
|
|
14796
14799
|
await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
|
|
14797
14800
|
this.pendingFileSnapshots = [];
|
|
@@ -14807,8 +14810,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
14807
14810
|
try {
|
|
14808
14811
|
await this.handle.datasync();
|
|
14809
14812
|
} catch (err) {
|
|
14810
|
-
|
|
14811
|
-
if (nodeErr?.code !== "EBADF") throw err;
|
|
14813
|
+
if (!isClosedHandleError(err)) throw err;
|
|
14812
14814
|
}
|
|
14813
14815
|
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14814
14816
|
const observedActivityMs = Date.parse(this.lastActivityAt);
|
|
@@ -28244,7 +28246,8 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
|
28244
28246
|
decision: "block",
|
|
28245
28247
|
reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
|
|
28246
28248
|
boardId: board.id,
|
|
28247
|
-
taskId: task.id
|
|
28249
|
+
taskId: task.id,
|
|
28250
|
+
readinessIssues: readiness.issues
|
|
28248
28251
|
};
|
|
28249
28252
|
}
|
|
28250
28253
|
if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
|
|
@@ -29035,7 +29038,8 @@ ${errorDetails}`,
|
|
|
29035
29038
|
type: "tool_result",
|
|
29036
29039
|
tool_use_id: use.id,
|
|
29037
29040
|
content: `Tool "${tool.name}" blocked by Kanban boundary. ${boundary.reason ?? ""}`.trim(),
|
|
29038
|
-
is_error: true
|
|
29041
|
+
is_error: true,
|
|
29042
|
+
_kanbanBoundary: boundary
|
|
29039
29043
|
};
|
|
29040
29044
|
budget = this.budgetForString(result.content, budget);
|
|
29041
29045
|
return { result, tool, durationMs: Date.now() - start };
|
package/dist/execution/index.js
CHANGED
|
@@ -21406,7 +21406,8 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
|
21406
21406
|
decision: "block",
|
|
21407
21407
|
reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
|
|
21408
21408
|
boardId: board.id,
|
|
21409
|
-
taskId: task.id
|
|
21409
|
+
taskId: task.id,
|
|
21410
|
+
readinessIssues: readiness.issues
|
|
21410
21411
|
};
|
|
21411
21412
|
}
|
|
21412
21413
|
if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
|
|
@@ -22197,7 +22198,8 @@ ${errorDetails}`,
|
|
|
22197
22198
|
type: "tool_result",
|
|
22198
22199
|
tool_use_id: use.id,
|
|
22199
22200
|
content: `Tool "${tool.name}" blocked by Kanban boundary. ${boundary.reason ?? ""}`.trim(),
|
|
22200
|
-
is_error: true
|
|
22201
|
+
is_error: true,
|
|
22202
|
+
_kanbanBoundary: boundary
|
|
22201
22203
|
};
|
|
22202
22204
|
budget = this.budgetForString(result.content, budget);
|
|
22203
22205
|
return { result, tool, durationMs: Date.now() - start };
|
package/dist/index.js
CHANGED
|
@@ -32528,6 +32528,44 @@ function userInputTitle(content) {
|
|
|
32528
32528
|
return sessionContentPreview(content, 60);
|
|
32529
32529
|
}
|
|
32530
32530
|
|
|
32531
|
+
// src/storage/session-writer-scrubber.ts
|
|
32532
|
+
function scrubSessionWriterEvent(event, secretScrubber) {
|
|
32533
|
+
const persistMessage = (message) => {
|
|
32534
|
+
const { _estTokens: _ignored, ...persisted } = message;
|
|
32535
|
+
return {
|
|
32536
|
+
...persisted,
|
|
32537
|
+
content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
|
|
32538
|
+
};
|
|
32539
|
+
};
|
|
32540
|
+
if (event.type === "context_snapshot" || event.type === "messages_replaced") {
|
|
32541
|
+
return { ...event, messages: event.messages.map(persistMessage) };
|
|
32542
|
+
}
|
|
32543
|
+
if (event.type === "message_appended" || event.type === "message_updated") {
|
|
32544
|
+
return { ...event, message: persistMessage(event.message) };
|
|
32545
|
+
}
|
|
32546
|
+
if (!secretScrubber) return event;
|
|
32547
|
+
if (event.type === "user_input") {
|
|
32548
|
+
return {
|
|
32549
|
+
...event,
|
|
32550
|
+
content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
|
|
32551
|
+
};
|
|
32552
|
+
}
|
|
32553
|
+
if (event.type === "llm_response") {
|
|
32554
|
+
return { ...event, content: secretScrubber.scrubObject(event.content) };
|
|
32555
|
+
}
|
|
32556
|
+
if (event.type === "file_snapshot") {
|
|
32557
|
+
return {
|
|
32558
|
+
...event,
|
|
32559
|
+
files: event.files.map((f) => ({
|
|
32560
|
+
...f,
|
|
32561
|
+
before: f.before !== null ? secretScrubber.scrub(f.before) : null,
|
|
32562
|
+
after: f.after !== null ? secretScrubber.scrub(f.after) : null
|
|
32563
|
+
}))
|
|
32564
|
+
};
|
|
32565
|
+
}
|
|
32566
|
+
return event;
|
|
32567
|
+
}
|
|
32568
|
+
|
|
32531
32569
|
// src/storage/session-writer-truncate.ts
|
|
32532
32570
|
import * as fsp11 from "node:fs/promises";
|
|
32533
32571
|
var CHUNK_SIZE = 65536;
|
|
@@ -32672,45 +32710,11 @@ async function rewriteSessionToCheckpoint(filePath, checkpointByteOffset) {
|
|
|
32672
32710
|
}
|
|
32673
32711
|
}
|
|
32674
32712
|
|
|
32675
|
-
// src/storage/session-writer-scrubber.ts
|
|
32676
|
-
function scrubSessionWriterEvent(event, secretScrubber) {
|
|
32677
|
-
const persistMessage = (message) => {
|
|
32678
|
-
const { _estTokens: _ignored, ...persisted } = message;
|
|
32679
|
-
return {
|
|
32680
|
-
...persisted,
|
|
32681
|
-
content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
|
|
32682
|
-
};
|
|
32683
|
-
};
|
|
32684
|
-
if (event.type === "context_snapshot" || event.type === "messages_replaced") {
|
|
32685
|
-
return { ...event, messages: event.messages.map(persistMessage) };
|
|
32686
|
-
}
|
|
32687
|
-
if (event.type === "message_appended" || event.type === "message_updated") {
|
|
32688
|
-
return { ...event, message: persistMessage(event.message) };
|
|
32689
|
-
}
|
|
32690
|
-
if (!secretScrubber) return event;
|
|
32691
|
-
if (event.type === "user_input") {
|
|
32692
|
-
return {
|
|
32693
|
-
...event,
|
|
32694
|
-
content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
|
|
32695
|
-
};
|
|
32696
|
-
}
|
|
32697
|
-
if (event.type === "llm_response") {
|
|
32698
|
-
return { ...event, content: secretScrubber.scrubObject(event.content) };
|
|
32699
|
-
}
|
|
32700
|
-
if (event.type === "file_snapshot") {
|
|
32701
|
-
return {
|
|
32702
|
-
...event,
|
|
32703
|
-
files: event.files.map((f) => ({
|
|
32704
|
-
...f,
|
|
32705
|
-
before: f.before !== null ? secretScrubber.scrub(f.before) : null,
|
|
32706
|
-
after: f.after !== null ? secretScrubber.scrub(f.after) : null
|
|
32707
|
-
}))
|
|
32708
|
-
};
|
|
32709
|
-
}
|
|
32710
|
-
return event;
|
|
32711
|
-
}
|
|
32712
|
-
|
|
32713
32713
|
// src/storage/file-session-writer.ts
|
|
32714
|
+
function isClosedHandleError(err) {
|
|
32715
|
+
const code = err?.code;
|
|
32716
|
+
return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
|
|
32717
|
+
}
|
|
32714
32718
|
var FileSessionWriter = class _FileSessionWriter {
|
|
32715
32719
|
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
32716
32720
|
this.id = id;
|
|
@@ -32882,8 +32886,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
32882
32886
|
try {
|
|
32883
32887
|
return await this.handle.appendFile(data, "utf8");
|
|
32884
32888
|
} catch (err) {
|
|
32885
|
-
|
|
32886
|
-
if (nodeErr?.code === "EBADF") {
|
|
32889
|
+
if (isClosedHandleError(err)) {
|
|
32887
32890
|
this.handle = await fsp12.open(this.filePath, "a", 384);
|
|
32888
32891
|
return await this.handle.appendFile(data, "utf8");
|
|
32889
32892
|
}
|
|
@@ -32923,8 +32926,8 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
32923
32926
|
bufferSynchronousEvent(event) {
|
|
32924
32927
|
if (this.closed) return;
|
|
32925
32928
|
void this.ensureInit();
|
|
32926
|
-
this.
|
|
32927
|
-
|
|
32929
|
+
const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
|
|
32930
|
+
this.observeForSummary(appendEvent);
|
|
32928
32931
|
try {
|
|
32929
32932
|
this._onAppend?.(appendEvent);
|
|
32930
32933
|
} catch {
|
|
@@ -33077,8 +33080,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
33077
33080
|
try {
|
|
33078
33081
|
await this.handle.datasync();
|
|
33079
33082
|
} catch (err) {
|
|
33080
|
-
|
|
33081
|
-
if (nodeErr?.code === "EBADF") {
|
|
33083
|
+
if (isClosedHandleError(err)) {
|
|
33082
33084
|
this.handle = await fsp12.open(this.filePath, "a", 384);
|
|
33083
33085
|
return;
|
|
33084
33086
|
}
|
|
@@ -33315,6 +33317,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
33315
33317
|
return this.closePromise;
|
|
33316
33318
|
}
|
|
33317
33319
|
async doClose() {
|
|
33320
|
+
await this.ensureInit();
|
|
33318
33321
|
if (this.pendingFileSnapshots.length > 0) {
|
|
33319
33322
|
await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
|
|
33320
33323
|
this.pendingFileSnapshots = [];
|
|
@@ -33330,8 +33333,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
33330
33333
|
try {
|
|
33331
33334
|
await this.handle.datasync();
|
|
33332
33335
|
} catch (err) {
|
|
33333
|
-
|
|
33334
|
-
if (nodeErr?.code !== "EBADF") throw err;
|
|
33336
|
+
if (!isClosedHandleError(err)) throw err;
|
|
33335
33337
|
}
|
|
33336
33338
|
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
33337
33339
|
const observedActivityMs = Date.parse(this.lastActivityAt);
|
|
@@ -63237,7 +63239,8 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
|
63237
63239
|
decision: "block",
|
|
63238
63240
|
reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
|
|
63239
63241
|
boardId: board.id,
|
|
63240
|
-
taskId: task.id
|
|
63242
|
+
taskId: task.id,
|
|
63243
|
+
readinessIssues: readiness.issues
|
|
63241
63244
|
};
|
|
63242
63245
|
}
|
|
63243
63246
|
if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
|
|
@@ -64047,7 +64050,8 @@ ${errorDetails}`,
|
|
|
64047
64050
|
type: "tool_result",
|
|
64048
64051
|
tool_use_id: use.id,
|
|
64049
64052
|
content: `Tool "${tool.name}" blocked by Kanban boundary. ${boundary.reason ?? ""}`.trim(),
|
|
64050
|
-
is_error: true
|
|
64053
|
+
is_error: true,
|
|
64054
|
+
_kanbanBoundary: boundary
|
|
64051
64055
|
};
|
|
64052
64056
|
budget = this.budgetForString(result.content, budget);
|
|
64053
64057
|
return { result, tool, durationMs: Date.now() - start };
|
|
@@ -89672,8 +89676,8 @@ var SessionRegistry = class {
|
|
|
89672
89676
|
if (id !== entry.sessionId) delete registry2[id];
|
|
89673
89677
|
continue;
|
|
89674
89678
|
}
|
|
89675
|
-
const
|
|
89676
|
-
if (
|
|
89679
|
+
const heartbeatAt = Date.parse(existing.lastHeartbeatAt);
|
|
89680
|
+
if (!Number.isFinite(heartbeatAt) || now - heartbeatAt > PID_CHECK_AFTER_MS && !pidAlive2(existing.pid)) {
|
|
89677
89681
|
delete registry2[id];
|
|
89678
89682
|
}
|
|
89679
89683
|
}
|
package/dist/security/index.js
CHANGED
|
@@ -1327,7 +1327,8 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
|
1327
1327
|
decision: "block",
|
|
1328
1328
|
reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
|
|
1329
1329
|
boardId: board.id,
|
|
1330
|
-
taskId: task.id
|
|
1330
|
+
taskId: task.id,
|
|
1331
|
+
readinessIssues: readiness.issues
|
|
1331
1332
|
};
|
|
1332
1333
|
}
|
|
1333
1334
|
if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { type KanbanBoundaryEvaluation } from '@wrongstack/kanban';
|
|
1
|
+
import { type KanbanBoundaryEvaluation, type KanbanContractReadinessIssue } from '@wrongstack/kanban';
|
|
2
2
|
import type { Context } from '../core/context.js';
|
|
3
3
|
import type { Tool } from '../types/tool.js';
|
|
4
4
|
export interface ToolKanbanBoundaryEvaluation extends KanbanBoundaryEvaluation {
|
|
5
5
|
boardId?: string | undefined;
|
|
6
6
|
taskId?: string | undefined;
|
|
7
|
+
/** Machine-readable readiness failures; `reason` remains the human fallback. */
|
|
8
|
+
readinessIssues?: KanbanContractReadinessIssue[] | undefined;
|
|
7
9
|
}
|
|
8
10
|
export interface ToolKanbanGovernanceOptions {
|
|
9
11
|
/** Require every product mutation to run inside a ready, running Kanban card. */
|
package/dist/storage/index.js
CHANGED
|
@@ -2427,8 +2427,8 @@ var SessionRegistry = class {
|
|
|
2427
2427
|
if (id !== entry.sessionId) delete registry[id];
|
|
2428
2428
|
continue;
|
|
2429
2429
|
}
|
|
2430
|
-
const
|
|
2431
|
-
if (
|
|
2430
|
+
const heartbeatAt = Date.parse(existing.lastHeartbeatAt);
|
|
2431
|
+
if (!Number.isFinite(heartbeatAt) || now - heartbeatAt > PID_CHECK_AFTER_MS && !pidAlive2(existing.pid)) {
|
|
2432
2432
|
delete registry[id];
|
|
2433
2433
|
}
|
|
2434
2434
|
}
|
|
@@ -11491,6 +11491,44 @@ import * as fsp11 from "node:fs/promises";
|
|
|
11491
11491
|
import * as path27 from "node:path";
|
|
11492
11492
|
import { createInterface as createInterface2 } from "node:readline";
|
|
11493
11493
|
|
|
11494
|
+
// src/storage/session-writer-scrubber.ts
|
|
11495
|
+
function scrubSessionWriterEvent(event, secretScrubber) {
|
|
11496
|
+
const persistMessage = (message) => {
|
|
11497
|
+
const { _estTokens: _ignored, ...persisted } = message;
|
|
11498
|
+
return {
|
|
11499
|
+
...persisted,
|
|
11500
|
+
content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
|
|
11501
|
+
};
|
|
11502
|
+
};
|
|
11503
|
+
if (event.type === "context_snapshot" || event.type === "messages_replaced") {
|
|
11504
|
+
return { ...event, messages: event.messages.map(persistMessage) };
|
|
11505
|
+
}
|
|
11506
|
+
if (event.type === "message_appended" || event.type === "message_updated") {
|
|
11507
|
+
return { ...event, message: persistMessage(event.message) };
|
|
11508
|
+
}
|
|
11509
|
+
if (!secretScrubber) return event;
|
|
11510
|
+
if (event.type === "user_input") {
|
|
11511
|
+
return {
|
|
11512
|
+
...event,
|
|
11513
|
+
content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
|
|
11514
|
+
};
|
|
11515
|
+
}
|
|
11516
|
+
if (event.type === "llm_response") {
|
|
11517
|
+
return { ...event, content: secretScrubber.scrubObject(event.content) };
|
|
11518
|
+
}
|
|
11519
|
+
if (event.type === "file_snapshot") {
|
|
11520
|
+
return {
|
|
11521
|
+
...event,
|
|
11522
|
+
files: event.files.map((f) => ({
|
|
11523
|
+
...f,
|
|
11524
|
+
before: f.before !== null ? secretScrubber.scrub(f.before) : null,
|
|
11525
|
+
after: f.after !== null ? secretScrubber.scrub(f.after) : null
|
|
11526
|
+
}))
|
|
11527
|
+
};
|
|
11528
|
+
}
|
|
11529
|
+
return event;
|
|
11530
|
+
}
|
|
11531
|
+
|
|
11494
11532
|
// src/storage/session-writer-truncate.ts
|
|
11495
11533
|
import * as fsp10 from "node:fs/promises";
|
|
11496
11534
|
var CHUNK_SIZE = 65536;
|
|
@@ -11635,45 +11673,11 @@ async function rewriteSessionToCheckpoint(filePath, checkpointByteOffset) {
|
|
|
11635
11673
|
}
|
|
11636
11674
|
}
|
|
11637
11675
|
|
|
11638
|
-
// src/storage/session-writer-scrubber.ts
|
|
11639
|
-
function scrubSessionWriterEvent(event, secretScrubber) {
|
|
11640
|
-
const persistMessage = (message) => {
|
|
11641
|
-
const { _estTokens: _ignored, ...persisted } = message;
|
|
11642
|
-
return {
|
|
11643
|
-
...persisted,
|
|
11644
|
-
content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
|
|
11645
|
-
};
|
|
11646
|
-
};
|
|
11647
|
-
if (event.type === "context_snapshot" || event.type === "messages_replaced") {
|
|
11648
|
-
return { ...event, messages: event.messages.map(persistMessage) };
|
|
11649
|
-
}
|
|
11650
|
-
if (event.type === "message_appended" || event.type === "message_updated") {
|
|
11651
|
-
return { ...event, message: persistMessage(event.message) };
|
|
11652
|
-
}
|
|
11653
|
-
if (!secretScrubber) return event;
|
|
11654
|
-
if (event.type === "user_input") {
|
|
11655
|
-
return {
|
|
11656
|
-
...event,
|
|
11657
|
-
content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
|
|
11658
|
-
};
|
|
11659
|
-
}
|
|
11660
|
-
if (event.type === "llm_response") {
|
|
11661
|
-
return { ...event, content: secretScrubber.scrubObject(event.content) };
|
|
11662
|
-
}
|
|
11663
|
-
if (event.type === "file_snapshot") {
|
|
11664
|
-
return {
|
|
11665
|
-
...event,
|
|
11666
|
-
files: event.files.map((f) => ({
|
|
11667
|
-
...f,
|
|
11668
|
-
before: f.before !== null ? secretScrubber.scrub(f.before) : null,
|
|
11669
|
-
after: f.after !== null ? secretScrubber.scrub(f.after) : null
|
|
11670
|
-
}))
|
|
11671
|
-
};
|
|
11672
|
-
}
|
|
11673
|
-
return event;
|
|
11674
|
-
}
|
|
11675
|
-
|
|
11676
11676
|
// src/storage/file-session-writer.ts
|
|
11677
|
+
function isClosedHandleError(err) {
|
|
11678
|
+
const code = err?.code;
|
|
11679
|
+
return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
|
|
11680
|
+
}
|
|
11677
11681
|
var FileSessionWriter = class _FileSessionWriter {
|
|
11678
11682
|
constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
|
|
11679
11683
|
this.id = id;
|
|
@@ -11845,8 +11849,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11845
11849
|
try {
|
|
11846
11850
|
return await this.handle.appendFile(data, "utf8");
|
|
11847
11851
|
} catch (err) {
|
|
11848
|
-
|
|
11849
|
-
if (nodeErr?.code === "EBADF") {
|
|
11852
|
+
if (isClosedHandleError(err)) {
|
|
11850
11853
|
this.handle = await fsp11.open(this.filePath, "a", 384);
|
|
11851
11854
|
return await this.handle.appendFile(data, "utf8");
|
|
11852
11855
|
}
|
|
@@ -11886,8 +11889,8 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
11886
11889
|
bufferSynchronousEvent(event) {
|
|
11887
11890
|
if (this.closed) return;
|
|
11888
11891
|
void this.ensureInit();
|
|
11889
|
-
this.
|
|
11890
|
-
|
|
11892
|
+
const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
|
|
11893
|
+
this.observeForSummary(appendEvent);
|
|
11891
11894
|
try {
|
|
11892
11895
|
this._onAppend?.(appendEvent);
|
|
11893
11896
|
} catch {
|
|
@@ -12040,8 +12043,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
12040
12043
|
try {
|
|
12041
12044
|
await this.handle.datasync();
|
|
12042
12045
|
} catch (err) {
|
|
12043
|
-
|
|
12044
|
-
if (nodeErr?.code === "EBADF") {
|
|
12046
|
+
if (isClosedHandleError(err)) {
|
|
12045
12047
|
this.handle = await fsp11.open(this.filePath, "a", 384);
|
|
12046
12048
|
return;
|
|
12047
12049
|
}
|
|
@@ -12278,6 +12280,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
12278
12280
|
return this.closePromise;
|
|
12279
12281
|
}
|
|
12280
12282
|
async doClose() {
|
|
12283
|
+
await this.ensureInit();
|
|
12281
12284
|
if (this.pendingFileSnapshots.length > 0) {
|
|
12282
12285
|
await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
|
|
12283
12286
|
this.pendingFileSnapshots = [];
|
|
@@ -12293,8 +12296,7 @@ var FileSessionWriter = class _FileSessionWriter {
|
|
|
12293
12296
|
try {
|
|
12294
12297
|
await this.handle.datasync();
|
|
12295
12298
|
} catch (err) {
|
|
12296
|
-
|
|
12297
|
-
if (nodeErr?.code !== "EBADF") throw err;
|
|
12299
|
+
if (!isClosedHandleError(err)) throw err;
|
|
12298
12300
|
}
|
|
12299
12301
|
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12300
12302
|
const observedActivityMs = Date.parse(this.lastActivityAt);
|
package/dist/types/blocks.d.ts
CHANGED
|
@@ -53,6 +53,15 @@ export interface ToolResultBlock {
|
|
|
53
53
|
* to the provider (it's an internal extension, not part of the wire format).
|
|
54
54
|
*/
|
|
55
55
|
_toolErrorInfo?: import('./tool.js').ToolErrorInfo | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Structured Kanban denial details for runtime callers and observability.
|
|
58
|
+
* Provider adapters deliberately serialize only the human-facing `content`.
|
|
59
|
+
*/
|
|
60
|
+
_kanbanBoundary?: (import('@wrongstack/kanban').KanbanBoundaryEvaluation & {
|
|
61
|
+
boardId?: string | undefined;
|
|
62
|
+
taskId?: string | undefined;
|
|
63
|
+
readinessIssues?: import('@wrongstack/kanban').KanbanContractReadinessIssue[] | undefined;
|
|
64
|
+
}) | undefined;
|
|
56
65
|
}
|
|
57
66
|
export interface ImageBlock {
|
|
58
67
|
type: 'image';
|
|
@@ -6,6 +6,9 @@ Scope:
|
|
|
6
6
|
- Review a diff for correctness bugs, edge cases, and regressions first
|
|
7
7
|
- Check error handling, resource cleanup, and concurrency hazards
|
|
8
8
|
- Assess readability, naming, and adherence to project conventions
|
|
9
|
+
- Flag cost-ladder violations: code that re-implements what the repo, the
|
|
10
|
+
language, the platform, or an installed dependency already provides; an
|
|
11
|
+
abstraction with a single caller; a new dependency bought for a few lines
|
|
9
12
|
- Separate must-fix from nice-to-have
|
|
10
13
|
|
|
11
14
|
Input format you accept:
|
|
@@ -11,6 +11,11 @@ self-contained handoff; do not take over fleet orchestration.
|
|
|
11
11
|
project's existing conventions, tests, and tooling.
|
|
12
12
|
- Make only task-relevant changes. Preserve unrelated work and avoid broad
|
|
13
13
|
refactors, dependency changes, generated churn, or formatting noise.
|
|
14
|
+
- Reach for new code last (the cost ladder). Prefer deleting over adding, reuse
|
|
15
|
+
what the repo, the language, the platform, or an installed dependency already
|
|
16
|
+
provides, and write it yourself only when none of them fit — then write the
|
|
17
|
+
smallest version. A new dependency needs an explicit grant. The ladder trims
|
|
18
|
+
code you invented; it never shrinks the assigned deliverable.
|
|
14
19
|
- Routine project-local reads, edits, and verification are pre-authorized when
|
|
15
20
|
the task permits implementation. Review, research, diagnosis, and planning
|
|
16
21
|
assignments remain read-only.
|
|
@@ -55,7 +60,11 @@ Execute the assigned task yourself; subagents do not orchestrate other workers.
|
|
|
55
60
|
|
|
56
61
|
If the task is too large, finish a clean and useful checkpoint. Submit
|
|
57
62
|
`completion:"partial"` with a concrete `remaining_work` description that a
|
|
58
|
-
fresh worker can execute.
|
|
63
|
+
fresh worker can execute. The same applies when verification refuses the same
|
|
64
|
+
work twice: stop retrying, and report `completion:"partial"` naming what was
|
|
65
|
+
refused and what the work still needs. A third identical attempt is never the
|
|
66
|
+
answer, and a refusal you cannot clear is a result to report, not a reason to
|
|
67
|
+
loop or to claim success. If an independent helper would materially improve
|
|
59
68
|
the outcome, ask the Director through the mailbox control-plane route with the
|
|
60
69
|
exact helper task, why it is independent, and the required output; continue
|
|
61
70
|
your own slice unless blocked.
|
|
@@ -14,11 +14,13 @@ The user is an experienced developer; accelerate them and stay focused.
|
|
|
14
14
|
5. Prefer surgical edits over rewrites.
|
|
15
15
|
6. Do not change unrelated code.
|
|
16
16
|
7. Match the file's existing conventions; add a dependency only when the task requires it.
|
|
17
|
-
8.
|
|
18
|
-
9.
|
|
19
|
-
10.
|
|
20
|
-
11.
|
|
21
|
-
12.
|
|
17
|
+
8. The cost ladder — before writing new code, stop at the first rung that answers: can it be deleted instead; does it need to exist; does this repo already do it; does the language, runtime, or platform do it; does an installed dependency do it; is it one line? Only then write the minimum that works.
|
|
18
|
+
9. The ladder trims code you invented, never the user's request. Reuse claims need a named file, symbol, or package — not recollection. Do not narrate rung numbers.
|
|
19
|
+
10. Do not claim checks passed unless you ran them.
|
|
20
|
+
11. Separate verified facts from assumptions and unknowns.
|
|
21
|
+
12. An empty search result is an answer — adjust the query instead of repeating the identical call.
|
|
22
|
+
13. Keep responses concise and scannable.
|
|
23
|
+
14. Match the user's language.
|
|
22
24
|
|
|
23
25
|
## Working loop
|
|
24
26
|
|
|
@@ -69,6 +71,7 @@ These apply to what you write on the board, not to whether you may work; none is
|
|
|
69
71
|
3. **Keep the board current as you go.** Record the transition, comment, check result or link on the card itself, not only in chat, as the work happens. Do not leave finished work sitting in Running. Updating the card follows the action; it does not authorize it.
|
|
70
72
|
4. **Managed boards have a fixed column order.** Cards move `Backlog → Todo → Running → Review → Done`, one step at a time. If a transition is refused, the message names the field it wants — supply it and retry, or use the `kanban` action `release_managed_lifecycle` to return the board to plain tracking (cards and history are kept).
|
|
71
73
|
5. **Never shrink tracked scope by omission.** Todo, task, and plan rows carry Kanban requirement identity. Preserve every unfinished row and binding in full-list updates, and complete it before removal.
|
|
74
|
+
6. **Two refusals park the card — they never park you.** Verification guards Done, not progress. The board counts each refusal and parks the card at the second one; read the recorded reason, then fix exactly what it names or move to the next ready card. Never retry a parked card unchanged. Parking is durable and honest — not Done, not abandoned, and never a way to shed scope.
|
|
72
75
|
|
|
73
76
|
<!--ws:end-->
|
|
74
77
|
|
|
@@ -108,6 +108,27 @@ Reasoning depth is a dial, not a constant. Match it to the blast radius of what
|
|
|
108
108
|
11. **Leave the knowledge behind, not just the diff.** A task that taught you something durable about this codebase isn't finished until that knowledge is in memory (see Memory management).
|
|
109
109
|
12. **Keep helper scripts temporary and contained.** This rule applies to every agent, regardless of role (leader, coordinator, or subagent). Create all ad hoc helper scripts and their temporary inputs/outputs only under `<project-root>/.temp_files/` — never in the repository root or source directories. Write each helper script so its paths, imports, and generated artifacts work from that location. Delete the helper script and any temporary artifacts it created as soon as they are no longer needed, and always before reporting the task complete. Only remove files created for the current task; never delete pre-existing or user-owned contents of `.temp_files/`. This rule does not apply to permanent project scripts explicitly requested by the user.
|
|
110
110
|
|
|
111
|
+
## The cost ladder
|
|
112
|
+
|
|
113
|
+
The five questions above decide *whether the change is right*. This ladder decides *how much code it costs*. Before you write any new code — a function, a wrapper, a flag, a fallback path, a file — walk it in order and stop at the first rung that answers. Each rung down costs more to write, review, test, document, and eventually delete. The cheapest code in this repository is the code you did not write; the second cheapest is the code you deleted.
|
|
114
|
+
|
|
115
|
+
0. **Delete instead?** If removing code satisfies the request, that is the change. A net-negative diff that still passes is the best outcome available. Limit: delete what you have *read and understood*, never what merely looks unused — an unreferenced symbol may be reached by dynamic dispatch, a plugin, a test fixture, or a published entry point.
|
|
116
|
+
1. **Does it need to exist?** No speculative generality: no options object with one caller, no interface with one implementation, no config flag nobody asked for, no guard against a state that cannot occur, no error path for an error the type system already excludes. An abstraction earns its keep at the third caller, not the first — until then, duplication is cheaper than the wrong shape.
|
|
117
|
+
2. **Does this repo already do it?** Reuse it even when yours would be nicer — a second implementation of one idea is a bug that hasn't happened yet, because only one of the two will get the next fix. If the existing one is close but wrong, fix it in place and update its callers instead of forking it.
|
|
118
|
+
<!--ws:if tool=codebase-search-->
|
|
119
|
+
Answer this rung with `codebase-search` rather than recollection.
|
|
120
|
+
<!--ws:end-->
|
|
121
|
+
<!--ws:if tool=detect_duplicate_code-->
|
|
122
|
+
For a change that adds a sizable helper, `detect_duplicate_code` tells you whether you just re-invented one.
|
|
123
|
+
<!--ws:end-->
|
|
124
|
+
3. **Does the language or runtime do it?** Standard library and built-ins before hand-rolled utilities.
|
|
125
|
+
4. **Does the platform do it?** The OS, shell, filesystem, terminal, or browser already implements most of what a utility module would — and its version handles the edge cases yours will not.
|
|
126
|
+
5. **Does an installed dependency do it?** Read the manifest before reaching outward. A package already in the tree is free; a new one costs install size, audit surface, upgrade work, and a licence question.
|
|
127
|
+
6. **Is it one line?** Then it is one line: no helper, no wrapper, no abstraction layer around it, no options bag, no barrel re-export.
|
|
128
|
+
7. **Only now, write the minimum that works** — the smallest thing that satisfies the stated requirement and its verification target, in the surrounding file's idiom.
|
|
129
|
+
|
|
130
|
+
**Guardrails.** The ladder trims what **you** invented; it never shrinks what the user asked for — rung 1 is not a licence to deliver less than the request. If you believe the request itself is unnecessary, say so in one sentence and build it anyway. Rungs 2–5 need evidence, not recollection: name the file, symbol, or package you are reusing, because "I think we have something like that" is rung 7 in disguise. A new dependency is the user's decision, proposed with the reason and the alternative you rejected — never installed as a side effect. Run the ladder silently: report the change, not which rung you stopped at, unless the user asks.
|
|
131
|
+
|
|
111
132
|
<!--ws:if tool=todo-->
|
|
112
133
|
## Todo status lifecycle
|
|
113
134
|
|
|
@@ -154,6 +175,11 @@ These apply to what you write on the board, not to whether you may work. They ex
|
|
|
154
175
|
3. **Keep the board current as you go.** Move a card to Running when you actually start it, to Review when the work is done, and to Done once accepted; record the transition, comment, check result or link on the card itself rather than only in chat. Update it as the work happens instead of reconstructing it afterwards, and do not leave finished work sitting in Running. Updating the card follows the action; it does not authorize it.
|
|
155
176
|
4. **Managed boards have a fixed column order.** On a board in managed mode, cards move `Backlog → Todo → Running → Review → Done`, one step at a time. If a transition is refused, the message names the field or action it wants — supply that and retry. If the ceremony is not serving this work, the `kanban` action `release_managed_lifecycle` returns the board to plain tracking; cards and history are kept.
|
|
156
177
|
5. **Never shrink tracked scope by omission.** Todo, task, and plan rows are identity-bearing projections of Kanban requirements, not disposable prose. Keep every unfinished row and its board/task binding in full-list updates; complete it before removal.
|
|
178
|
+
6. **Two refusals park the card — they never park you.** Verification guards *Done*, not *progress*. This is the task-level form of the rule you already apply to tools: two failures in the same place mean your model is wrong, so a third identical attempt is not the answer. Every refusal from the completion gate or a `done` transition is counted on the card, and at the second one the board parks it and records what was refused. You do not park a card by hand and you do not argue with the gate: read the recorded reason, then either fix the exact thing it names or move to the next ready card. A parked card is an honest durable state — not Done, not abandoned, not a reason to stop working. Return to it when its blocker clears or when nothing else is ready.
|
|
179
|
+
|
|
180
|
+
Parking records that a card needs something you do not have; it never sheds scope. A criterion that turned out not to apply is a `remove_check`, not a park, and re-running an unchanged card to burn its budget is worse than reporting the refusal. If every remaining card is parked, say so plainly instead of reporting the work complete; a board of parked cards is a result the user needs to see, not a failure to hide.
|
|
181
|
+
|
|
182
|
+
A card waiting on a parked dependency is blocked for a real reason. Two honest moves exist and you must name which you took: clear the parked card, or correct the `dependsOn` because the dependency should never have been recorded. Silently working around a parked dependency is neither.
|
|
157
183
|
|
|
158
184
|
## Kanban scenarios and lifecycle
|
|
159
185
|
|
package/instructions/system.md
CHANGED
|
@@ -49,6 +49,24 @@ This parse is **internal reasoning**, not something you output. It keeps you anc
|
|
|
49
49
|
9. **The working tree is shared.** Never commit, push, amend, or discard changes unless the user asked for it. Treat destructive commands (recursive delete, hard reset, force push, history rewrites) as requiring an explicit request — never run them as convenience cleanup.
|
|
50
50
|
10. **Keep helper scripts temporary and contained.** This rule applies to every agent, regardless of role (leader, coordinator, or subagent). Create all ad hoc helper scripts and their temporary inputs/outputs only under `<project-root>/.temp_files/` — never in the repository root or source directories. Write each helper script so its paths, imports, and generated artifacts work from that location. Delete the helper script and any temporary artifacts it created as soon as they are no longer needed, and always before reporting the task complete. Only remove files created for the current task; never delete pre-existing or user-owned contents of `.temp_files/`. This rule does not apply to permanent project scripts explicitly requested by the user.
|
|
51
51
|
|
|
52
|
+
## The cost ladder
|
|
53
|
+
|
|
54
|
+
Before you write any new code — a function, a wrapper, a flag, a fallback path, a file — walk this ladder in order and stop at the first rung that answers. Each rung down costs more to write, review, test, and eventually delete; you are spending the user's future time, not just this turn.
|
|
55
|
+
|
|
56
|
+
0. **Delete instead?** If removing code satisfies the request, that is the change. A net-negative diff that still passes is the best outcome available.
|
|
57
|
+
1. **Does it need to exist?** No speculative generality: no options object with one caller, no interface with one implementation, no config flag nobody asked for, no guard against a state that cannot occur.
|
|
58
|
+
2. **Does this repo already do it?** Reuse it even when yours would be nicer — a second implementation of one idea is a bug that hasn't happened yet. If the existing one is close but wrong, fix it in place instead of forking it.
|
|
59
|
+
<!--ws:if tool=codebase-search-->
|
|
60
|
+
Confirm with `codebase-search` before writing a new helper; the index answers this rung faster than memory does.
|
|
61
|
+
<!--ws:end-->
|
|
62
|
+
3. **Does the language or runtime do it?** Standard library and built-ins before hand-rolled utilities.
|
|
63
|
+
4. **Does the platform do it?** The OS, shell, filesystem, terminal, or browser already implements most of what a utility module would.
|
|
64
|
+
5. **Does an installed dependency do it?** Read the manifest before reaching outward — a package you already ship is free, a new one is not.
|
|
65
|
+
6. **Is it one line?** Then it is one line: no helper, no wrapper, no abstraction layer around it.
|
|
66
|
+
7. **Only now, write the minimum that works** — the smallest thing that satisfies the stated requirement and its verification, in the surrounding file's idiom.
|
|
67
|
+
|
|
68
|
+
The ladder trims what **you** invented; it never shrinks what the user asked for. If you believe the request itself is unnecessary, say so in one sentence and build it anyway. Rungs 2–5 need evidence, not recollection: name the file, symbol, or package you are reusing — "I think we have something like that" is rung 7 in disguise. A new dependency is the user's decision, never a side effect. Run the ladder silently; do not narrate rung numbers or lecture about it unless asked.
|
|
69
|
+
|
|
52
70
|
<!--ws:if tool=todo-->
|
|
53
71
|
## Todo status lifecycle
|
|
54
72
|
|
|
@@ -93,6 +111,9 @@ These apply to what you write on the board, not to whether you may work. They ex
|
|
|
93
111
|
3. **Keep the board current as you go.** Move a card to Running when you actually start it, to Review when the work is done, and to Done once accepted; record the transition, comment, check result or link on the card itself rather than only in chat. Update it as the work happens instead of reconstructing it afterwards, and do not leave finished work sitting in Running. Updating the card follows the action; it does not authorize it.
|
|
94
112
|
4. **Managed boards have a fixed column order.** On a board in managed mode, cards move `Backlog → Todo → Running → Review → Done`, one step at a time. If a transition is refused, the message names the field or action it wants — supply that and retry. If the ceremony is not serving this work, the `kanban` action `release_managed_lifecycle` returns the board to plain tracking; cards and history are kept.
|
|
95
113
|
5. **Never shrink tracked scope by omission.** Todo, task, and plan rows are identity-bearing projections of Kanban requirements, not disposable prose. Keep every unfinished row and its board/task binding in full-list updates; complete it before removal.
|
|
114
|
+
6. **Two refusals park the card — they never park you.** Verification guards *Done*, not *progress*. Every refusal from the completion gate or a `done` transition is counted on the card, and at the second one the board parks it and records what was refused. You do not park a card by hand and you do not argue with the gate: read the recorded reason, then either fix the exact thing it names or move to the next ready card. A parked card is an honest durable state — not Done, not abandoned, not a reason to stop working. Return to it when its blocker clears or when nothing else is ready.
|
|
115
|
+
|
|
116
|
+
Parking records that a card needs something you do not have; it never sheds scope. A criterion that turned out not to apply is a `remove_check`, not a park. If every remaining card is parked, say so plainly instead of reporting the work complete. A card waiting on a parked dependency is blocked for a real reason — either clear the parked card or correct its `dependsOn` deliberately, and say which you did.
|
|
96
117
|
|
|
97
118
|
## Kanban scenarios and lifecycle
|
|
98
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/core",
|
|
3
|
-
"version": "0.306.
|
|
3
|
+
"version": "0.306.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "WrongStack core: kernel, types, defaults, and shared utilities for the WrongStack CLI agent.",
|
|
6
6
|
"repository": {
|
|
@@ -177,8 +177,8 @@
|
|
|
177
177
|
"wrongstackApiVersion": "0.1.10",
|
|
178
178
|
"dependencies": {
|
|
179
179
|
"zod": "4.4.3",
|
|
180
|
-
"@wrongstack/persistence": "0.306.
|
|
181
|
-
"@wrongstack/kanban": "0.306.
|
|
180
|
+
"@wrongstack/persistence": "0.306.3",
|
|
181
|
+
"@wrongstack/kanban": "0.306.3"
|
|
182
182
|
},
|
|
183
183
|
"devDependencies": {
|
|
184
184
|
"@types/node": "^26.1.2",
|