@wrongstack/core 0.306.2 → 0.306.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/coordination/index.js +184 -86
  2. package/dist/coordination/mail-tools.d.ts +1 -1
  3. package/dist/coordination/mailbox-project-server.js +38 -16
  4. package/dist/coordination/sqlite-mailbox.d.ts +1 -0
  5. package/dist/core/conversation-state.d.ts +1 -2
  6. package/dist/core/fallback-model.d.ts +19 -1
  7. package/dist/core/fallback-profile-manager.d.ts +21 -3
  8. package/dist/core/index.d.ts +1 -1
  9. package/dist/core/index.js +152 -46
  10. package/dist/defaults/index.js +347 -139
  11. package/dist/execution/auto-compaction-middleware.d.ts +64 -0
  12. package/dist/execution/compaction-core.d.ts +4 -0
  13. package/dist/execution/index.d.ts +1 -1
  14. package/dist/execution/index.js +234 -91
  15. package/dist/execution/retry-policy.d.ts +27 -0
  16. package/dist/hq/index.js +50 -15
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +642 -184
  19. package/dist/infrastructure/index.js +21 -0
  20. package/dist/plugin/index.js +195 -19
  21. package/dist/security/index.js +52 -16
  22. package/dist/security/kanban-boundary.d.ts +3 -1
  23. package/dist/session-catalog/index.js +50 -15
  24. package/dist/session-catalog/project-server.js +50 -15
  25. package/dist/storage/cloud-config-sync/sanitize.d.ts +18 -0
  26. package/dist/storage/cloud-config-sync.d.ts +1 -1
  27. package/dist/storage/index.js +326 -71
  28. package/dist/storage/provider-config-watcher.d.ts +9 -0
  29. package/dist/storage/session-store/strict-empty-check.d.ts +7 -0
  30. package/dist/storage/session-store.d.ts +1 -0
  31. package/dist/tools/index.d.ts +1 -1
  32. package/dist/tools/index.js +83 -22
  33. package/dist/types/blocks.d.ts +9 -0
  34. package/dist/types/config/root.d.ts +14 -0
  35. package/dist/types/session.d.ts +7 -0
  36. package/instructions/agents/code-reviewer.md +3 -0
  37. package/instructions/coordination/subagent-baseline.md +10 -1
  38. package/instructions/system-lite.md +8 -5
  39. package/instructions/system-pro.md +26 -0
  40. package/instructions/system.md +21 -0
  41. package/package.json +3 -3
@@ -2024,6 +2024,12 @@ function normalizeModelRef(ref, defaultProvider) {
2024
2024
  function hasText(value) {
2025
2025
  return typeof value === "string" && value.trim().length > 0;
2026
2026
  }
2027
+ function asRefList(value) {
2028
+ return Array.isArray(value) ? value : void 0;
2029
+ }
2030
+ function asProfileName(value) {
2031
+ return hasText(value) ? value : void 0;
2032
+ }
2027
2033
  function providerHasKey(entry) {
2028
2034
  if (!entry) return false;
2029
2035
  if (hasText(entry.apiKey)) return true;
@@ -2034,7 +2040,7 @@ function providerHasKey(entry) {
2034
2040
  }
2035
2041
  function visibleProviderModels(config, providerId, providerModels) {
2036
2042
  const entry = config.providers?.[providerId];
2037
- return entry?.models !== void 0 ? [...entry.models] : providerModels;
2043
+ return Array.isArray(entry?.models) ? [...entry.models] : providerModels;
2038
2044
  }
2039
2045
  function buildProfiles(config) {
2040
2046
  const entries = /* @__PURE__ */ new Map();
@@ -2071,13 +2077,34 @@ var FallbackProfileManager = class {
2071
2077
  listProfiles() {
2072
2078
  return Object.freeze([...this.profiles.keys()]);
2073
2079
  }
2080
+ /**
2081
+ * The profile the session has selected (`config.fallbackProfile`, set by
2082
+ * `/fallback profile use <name>`), or undefined when none is selected or the
2083
+ * name no longer resolves to a defined profile.
2084
+ *
2085
+ * Consulted by every resolution entry point when the caller does not name a
2086
+ * profile itself. Without this the leader — which passes no profile — could
2087
+ * never use a named profile at all: `config.fallbackProfiles` was reachable
2088
+ * only by copying a chain into `fallbackModels`.
2089
+ */
2090
+ activeProfileName() {
2091
+ const name = asProfileName(this.config.fallbackProfile);
2092
+ return name && this.profiles.has(name) ? name : void 0;
2093
+ }
2074
2094
  // ── Resolution ─────────────────────────────────────────────────────────
2075
2095
  /**
2076
2096
  * Resolve a named fallback profile to a validated, provider-filtered chain.
2077
2097
  *
2078
- * Returns an empty chain when:
2079
- * - The profile doesn't exist.
2080
- * - Every entry's provider is missing, has no key, or has no matching model.
2098
+ * Returns an empty chain when the profile doesn't exist, or when every entry
2099
+ * is excluded, quarantined, or blacked out.
2100
+ *
2101
+ * Filtering is intentionally identical to {@link resolveRefs} (the explicit
2102
+ * `fallbackModels` path): the self-exclusion, the runtime status tracker,
2103
+ * and the availability calendar — nothing else. Anything a named profile
2104
+ * drops, an explicit chain drops too, and vice versa. Profiles used to apply
2105
+ * two extra filters (provider "usability" and the `providers[].models`
2106
+ * snapshot) that the explicit path did not, which silently rerouted roles
2107
+ * pinned to a profile onto a different model than the one configured.
2081
2108
  *
2082
2109
  * @param name - Profile name from config.fallbackProfiles.
2083
2110
  * @param defaultProvider - Used when an entry has no explicit provider.
@@ -2098,13 +2125,9 @@ var FallbackProfileManager = class {
2098
2125
  if (seen.has(key)) continue;
2099
2126
  seen.add(key);
2100
2127
  if (excludeKey && key === excludeKey) continue;
2101
- const health = this.checkProvider(providerId);
2102
- if (!health.usable) continue;
2103
2128
  if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
2104
2129
  if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
2105
2130
  continue;
2106
- const allowedModels = this.config.providers?.[providerId]?.models;
2107
- if (allowedModels && !allowedModels.includes(parsed.model)) continue;
2108
2131
  resolved.push({
2109
2132
  providerId,
2110
2133
  model: parsed.model,
@@ -2122,12 +2145,14 @@ var FallbackProfileManager = class {
2122
2145
  resolveEffective(opts = {}) {
2123
2146
  const bridge = this.resolveBridge(opts.exclude);
2124
2147
  let selected = FREEZER_EMPTY;
2125
- if (opts.fallbackModels && opts.fallbackModels.length > 0) {
2126
- const resolved = this.resolveRefs(opts.fallbackModels, opts.exclude);
2148
+ const explicitRefs = asRefList(opts.fallbackModels);
2149
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
2150
+ if (explicitRefs && explicitRefs.length > 0) {
2151
+ const resolved = this.resolveRefs(explicitRefs, opts.exclude);
2127
2152
  if (resolved.length > 0) selected = resolved;
2128
2153
  }
2129
- if (selected.length === 0 && opts.fallbackProfile) {
2130
- const resolved = this.resolve(opts.fallbackProfile, { exclude: opts.exclude });
2154
+ if (selected.length === 0 && profileName) {
2155
+ const resolved = this.resolve(profileName, { exclude: opts.exclude });
2131
2156
  if (resolved.length > 0) selected = resolved;
2132
2157
  }
2133
2158
  if (selected.length === 0 && opts.fallbackAuto !== false) {
@@ -2203,13 +2228,14 @@ var FallbackProfileManager = class {
2203
2228
  };
2204
2229
  const configFallbackAuto = this.config.fallbackAuto;
2205
2230
  const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
2206
- const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
2231
+ const explicitRefs = asRefList(opts.fallbackModels) ?? asRefList(this.config.fallbackModels);
2232
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
2207
2233
  const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
2208
- const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
2234
+ const profileUsable = profileName !== void 0 && this.hasProfile(profileName) && this.resolve(profileName, { exclude: current }).length > 0;
2209
2235
  const fromExplicitSource = explicitUsable || profileUsable;
2210
- const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
2236
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : profileName ? this.resolve(profileName, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
2211
2237
  fallbackModels: explicitRefs,
2212
- fallbackProfile: opts.fallbackProfile,
2238
+ fallbackProfile: profileName,
2213
2239
  fallbackAuto: effectiveFallbackAuto,
2214
2240
  exclude: current
2215
2241
  });
@@ -2226,7 +2252,7 @@ var FallbackProfileManager = class {
2226
2252
  });
2227
2253
  }
2228
2254
  candidates.push(...selectedChain);
2229
- if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
2255
+ if (!fromExplicitSource && effectiveFallbackAuto && profileName !== "default") {
2230
2256
  candidates.push(...this.resolve("default", { exclude: current }));
2231
2257
  }
2232
2258
  if (!fromExplicitSource && effectiveFallbackAuto) {
@@ -2304,7 +2330,7 @@ var FallbackProfileManager = class {
2304
2330
  const leaderModel = this.config.model;
2305
2331
  const providers = this.config.providers ?? {};
2306
2332
  const favoriteSet = new Set(
2307
- (this.config.favoriteModels ?? []).map((ref) => {
2333
+ (asRefList(this.config.favoriteModels) ?? []).map((ref) => {
2308
2334
  const p = parseModelRef(ref);
2309
2335
  return `${p.provider ?? leaderProvider}/${p.model}`;
2310
2336
  })
@@ -7478,6 +7504,30 @@ var PROVIDER_MANAGE_SCHEMA = {
7478
7504
  required: ["action"],
7479
7505
  additionalProperties: false
7480
7506
  };
7507
+ var CREDENTIAL_SELECTOR_FIELDS = ["apiKey", "apiKeys", "activeKey", "envVars"];
7508
+ function envVarsClaimedByOtherProviders(providers, exceptProvider) {
7509
+ const claimed = /* @__PURE__ */ new Map();
7510
+ for (const [id, entry] of Object.entries(providers)) {
7511
+ if (id === exceptProvider) continue;
7512
+ const names = entry?.["envVars"];
7513
+ if (!Array.isArray(names)) continue;
7514
+ for (const name of names) {
7515
+ if (typeof name === "string" && !claimed.has(name)) claimed.set(name, id);
7516
+ }
7517
+ }
7518
+ return claimed;
7519
+ }
7520
+ function rejectBorrowedEnvVars(providers, provider, requested) {
7521
+ if (!requested) return null;
7522
+ const claimed = envVarsClaimedByOtherProviders(providers, provider);
7523
+ for (const name of requested) {
7524
+ const owner = typeof name === "string" ? claimed.get(name) : void 0;
7525
+ if (owner !== void 0) {
7526
+ return `Environment variable "${String(name)}" already supplies the key for provider "${owner}". Reading another provider's credential from "${provider}" is not allowed; use provider_key_set to give "${provider}" its own key.`;
7527
+ }
7528
+ }
7529
+ return null;
7530
+ }
7481
7531
  function validateProviderBaseUrl(raw) {
7482
7532
  let url;
7483
7533
  try {
@@ -7548,6 +7598,8 @@ ${msg}`,
7548
7598
  const invalid = validateProviderBaseUrl(input.baseUrl);
7549
7599
  if (invalid) return { status: "error", message: invalid };
7550
7600
  }
7601
+ const borrowed = rejectBorrowedEnvVars(providers, input.provider, input.envVars);
7602
+ if (borrowed) return { status: "error", message: borrowed };
7551
7603
  const entry = { type: input.type };
7552
7604
  if (input.models) entry.models = input.models;
7553
7605
  if (input.baseUrl) entry.baseUrl = input.baseUrl;
@@ -7581,14 +7633,22 @@ ${msg}`,
7581
7633
  if (input.autoDiscoverModels !== void 0) entry.autoDiscoverModels = input.autoDiscoverModels;
7582
7634
  if (input.apiKey !== void 0) entry.apiKey = input.apiKey || void 0;
7583
7635
  const endpointChanged = input.baseUrl !== void 0 && (entry.baseUrl ?? void 0) !== (previous.baseUrl ?? void 0);
7584
- const keyDropped = endpointChanged && input.apiKey === void 0 && previous.apiKey !== void 0;
7585
- if (keyDropped) entry.apiKey = void 0;
7636
+ const explicitlySupplied = /* @__PURE__ */ new Set([
7637
+ ...input.apiKey !== void 0 ? ["apiKey"] : [],
7638
+ ...input.envVars !== void 0 ? ["envVars"] : []
7639
+ ]);
7640
+ const droppedFields = endpointChanged ? CREDENTIAL_SELECTOR_FIELDS.filter(
7641
+ (field) => !explicitlySupplied.has(field) && previous[field] !== void 0
7642
+ ) : [];
7643
+ for (const field of droppedFields) entry[field] = void 0;
7644
+ const borrowed = rejectBorrowedEnvVars(providers, input.provider, input.envVars);
7645
+ if (borrowed) return { status: "error", message: borrowed };
7586
7646
  providers[input.provider] = entry;
7587
7647
  await opts.updateConfig((cfg) => {
7588
7648
  cfg.providers = providers;
7589
7649
  });
7590
7650
  const updated = Object.keys({ ...entry }).filter((k) => k !== "apiKey").join(", ");
7591
- const keyNote = keyDropped ? " \u2014 stored API key cleared because the base URL changed; set it again with provider_key_set" : "";
7651
+ const keyNote = droppedFields.length > 0 ? ` \u2014 cleared ${droppedFields.join(", ")} because the base URL changed; set the key again with provider_key_set` : "";
7592
7652
  return { status: "ok", message: `\u2713 Updated ${input.provider}: ${updated}${keyNote}` };
7593
7653
  }
7594
7654
  if (input.action === "remove") {
@@ -9494,6 +9554,7 @@ export {
9494
9554
  createMcpControlTool,
9495
9555
  createMcpUseTool,
9496
9556
  createOneShotLLMTool,
9497
- createPluginManagerTool
9557
+ createPluginManagerTool,
9558
+ validateProviderBaseUrl
9498
9559
  };
9499
9560
  //# sourceMappingURL=index.js.map
@@ -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';
@@ -115,6 +115,20 @@ export interface Config {
115
115
  * model by `/setmodel`, while the whole ordered list is used for failover.
116
116
  */
117
117
  fallbackProfiles?: Record<string, string[]> | undefined;
118
+ /**
119
+ * The named profile from {@link Config.fallbackProfiles} currently selected
120
+ * for failover, set by `/fallback profile use <name>` and cleared by
121
+ * `/fallback profile none`.
122
+ *
123
+ * Resolution order is `fallbackModels` → this profile → smart default, so an
124
+ * explicit chain still wins. Selecting a profile does NOT overwrite
125
+ * `fallbackModels`: before this field existed, `profile use` was implemented
126
+ * by copying the profile's entries over the explicit chain, which destroyed
127
+ * whatever the user had configured and left no way back.
128
+ *
129
+ * A name that no longer matches a defined profile is ignored.
130
+ */
131
+ fallbackProfile?: string | undefined;
118
132
  /**
119
133
  * When `true` (the default) and `fallbackModels` is empty, a fallback chain
120
134
  * is derived automatically from the other keyed providers/models so 429s
@@ -594,6 +594,13 @@ export interface SessionStore {
594
594
  * Returns the refreshed summary. Throws if the session does not exist.
595
595
  */
596
596
  rename(id: string, name: string): Promise<SessionSummary>;
597
+ /**
598
+ * Return true only when the persisted journal is strictly readable and
599
+ * contains lifecycle envelope events but no messages or other session content.
600
+ * Implementations should fail closed (false) for malformed or unknown events.
601
+ * Optional stores that cannot make this guarantee must omit the method.
602
+ */
603
+ isEmpty?(id: string): Promise<boolean>;
597
604
  delete(id: string): Promise<void>;
598
605
  /**
599
606
  * Rewrite the session JSONL file to contain only a fresh session_start
@@ -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.4",
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/kanban": "0.306.4",
181
+ "@wrongstack/persistence": "0.306.4"
182
182
  },
183
183
  "devDependencies": {
184
184
  "@types/node": "^26.1.2",