@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.
@@ -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
- const nodeErr = err;
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.observeForSummary(event);
14983
- const appendEvent = event.type === "file_snapshot" ? scrubSessionWriterEvent(event, this.secretScrubber) : event;
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
- const nodeErr = err;
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
- const nodeErr = err;
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 useTargetedReceipts = rows.length <= 500;
1976
- const receiptSql = useTargetedReceipts ? `
1977
- SELECT message_id, actor_id, read_at, completed_at, completed_by, outcome
1978
- FROM message_receipts
1979
- WHERE message_id IN (${rows.map(() => "?").join(", ")})
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
- const receiptRows = db.prepare(receiptSql).all(...useTargetedReceipts ? rows.map((row) => row.id) : []);
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 statement = db.prepare("DELETE FROM messages WHERE id = ?");
2014
- for (const id of ids) statement.run(id);
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, chunk) {
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 chunk.split(LINE_SEPARATOR)) {
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
- return autoCompact(this.compactionCtx(), options);
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, chunk) {
3471
+ function onData(state, chunk2) {
3450
3472
  state.lastSeenAt = Date.now();
3451
- state.buffer += chunk;
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", (chunk) => onData(state, chunk));
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;
@@ -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
- const nodeErr = err;
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.observeForSummary(event);
14404
- const appendEvent = event.type === "file_snapshot" ? scrubSessionWriterEvent(event, this.secretScrubber) : event;
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
- const nodeErr = err;
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
- const nodeErr = err;
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 };
@@ -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
- const nodeErr = err;
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.observeForSummary(event);
32927
- const appendEvent = event.type === "file_snapshot" ? scrubSessionWriterEvent(event, this.secretScrubber) : event;
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
- const nodeErr = err;
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
- const nodeErr = err;
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 heartbeatAge = now - new Date(existing.lastHeartbeatAt).getTime();
89676
- if (heartbeatAge > PID_CHECK_AFTER_MS && !pidAlive2(existing.pid)) {
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
  }
@@ -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. */
@@ -2427,8 +2427,8 @@ var SessionRegistry = class {
2427
2427
  if (id !== entry.sessionId) delete registry[id];
2428
2428
  continue;
2429
2429
  }
2430
- const heartbeatAge = now - new Date(existing.lastHeartbeatAt).getTime();
2431
- if (heartbeatAge > PID_CHECK_AFTER_MS && !pidAlive2(existing.pid)) {
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
- const nodeErr = err;
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.observeForSummary(event);
11890
- const appendEvent = event.type === "file_snapshot" ? scrubSessionWriterEvent(event, this.secretScrubber) : event;
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
- const nodeErr = err;
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
- const nodeErr = err;
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);
@@ -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. If an independent helper would materially improve
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. Do not claim checks passed unless you ran them.
18
- 9. Separate verified facts from assumptions and unknowns.
19
- 10. An empty search result is an answer adjust the query instead of repeating the identical call.
20
- 11. Keep responses concise and scannable.
21
- 12. Match the user's language.
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
 
@@ -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.2",
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.2",
181
- "@wrongstack/kanban": "0.306.2"
180
+ "@wrongstack/persistence": "0.306.3",
181
+ "@wrongstack/kanban": "0.306.3"
182
182
  },
183
183
  "devDependencies": {
184
184
  "@types/node": "^26.1.2",