@wrongstack/core 0.309.1 → 0.310.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chronicle/index.js +113 -41
- package/dist/chronicle/metrics-ingest.d.ts +7 -0
- package/dist/chronicle/metrics-schema.d.ts +4 -1
- package/dist/chronicle/project-server.js +94 -38
- package/dist/chronicle/query.d.ts +1 -0
- package/dist/chronicle/sqlite-journal-schema.d.ts +2 -1
- package/dist/chronicle/types.d.ts +2 -0
- package/dist/{agent-status-helpers.d.ts → coordination/agent-status-helpers.d.ts} +1 -1
- package/dist/{agent-status-tracker.d.ts → coordination/agent-status-tracker.d.ts} +2 -2
- package/dist/{middleware → coordination}/collab-pause.d.ts +1 -1
- package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -1
- package/dist/coordination/fleet-status-tool.d.ts +1 -1
- package/dist/coordination/index.d.ts +5 -1
- package/dist/coordination/index.js +2061 -346
- package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
- package/dist/coordination/kanban-ops-port.d.ts +21 -0
- package/dist/coordination/mailbox-hooks.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +14 -9
- package/dist/core/context.d.ts +35 -44
- package/dist/core/conversation-state.d.ts +16 -52
- package/dist/core/index.js +53 -16
- package/dist/core/provider-runner.d.ts +2 -2
- package/dist/core/run-env.d.ts +7 -28
- package/dist/core/streaming-response-builder.d.ts +2 -2
- package/dist/execution/index.js +111 -153
- package/dist/goal/index.js +100 -22
- package/dist/hq/auth-store.d.ts +9 -0
- package/dist/hq/cost-bridge.d.ts +1 -1
- package/dist/hq/index.js +24 -2
- package/dist/index.d.ts +12 -5
- package/dist/index.js +27806 -33937
- package/dist/infrastructure/index.js +210 -132
- package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
- package/dist/infrastructure/token-counter.d.ts +5 -1
- package/dist/kernel/events/file-events.d.ts +6 -0
- package/dist/kernel/events/provider-events.d.ts +10 -7
- package/dist/kernel/events/session-events.d.ts +4 -4
- package/dist/kernel/events/tool-events.d.ts +12 -2
- package/dist/plugin/index.js +105 -35
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +47 -18
- package/dist/security/kanban-boundary.d.ts +1 -1
- package/dist/security/kanban-governance-port.d.ts +28 -0
- package/dist/session-catalog/index.js +2 -3
- package/dist/session-catalog/project-server.js +2 -3
- package/dist/session-catalog/protocol.d.ts +1 -1
- package/dist/session-catalog/registry.d.ts +1 -1
- package/dist/session-catalog/store-schema.d.ts +1 -1
- package/dist/session-catalog/store.d.ts +1 -1
- package/dist/storage/annotations-store.d.ts +1 -1
- package/dist/storage/board-store-port.d.ts +45 -0
- package/dist/storage/completed-work-checkpoint.d.ts +1 -1
- package/dist/storage/config-loader/types.d.ts +1 -1
- package/dist/storage/event-bus-port.d.ts +27 -0
- package/dist/storage/file-session-writer.d.ts +47 -5
- package/dist/storage/goal-coordination.d.ts +1 -1
- package/dist/storage/goal-store.d.ts +1 -1
- package/dist/storage/index.d.ts +3 -4
- package/dist/storage/index.js +1063 -1459
- package/dist/storage/plan-store.d.ts +1 -1
- package/dist/storage/queue-store.d.ts +1 -1
- package/dist/storage/replay-log-store.d.ts +1 -1
- package/dist/storage/session-recovery.d.ts +10 -0
- package/dist/storage/session-resume-validation.d.ts +13 -1
- package/dist/storage/session-store/events.d.ts +1 -1
- package/dist/storage/session-store/load-session-data.d.ts +1 -1
- package/dist/storage/session-store/rename-session.d.ts +1 -1
- package/dist/storage/session-store/resume-session.d.ts +3 -1
- package/dist/storage/session-store/session-store-index.d.ts +2 -1
- package/dist/storage/session-store/types.d.ts +1 -1
- package/dist/storage/session-store.d.ts +70 -0
- package/dist/storage/session-summary-tracker.d.ts +8 -0
- package/dist/storage/session-write-buffer.d.ts +31 -2
- package/dist/storage/task-store.d.ts +1 -1
- package/dist/storage/todos-checkpoint.d.ts +1 -1
- package/dist/storage/tool-audit-log.d.ts +1 -1
- package/dist/tasking/index.js +100 -22
- package/dist/tasking/task-tracker.d.ts +24 -1
- package/dist/types/compactor.d.ts +2 -2
- package/dist/types/context.d.ts +212 -0
- package/dist/types/conversation-state.d.ts +109 -0
- package/dist/types/error-handler.d.ts +2 -2
- package/dist/types/file-event-record.d.ts +6 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +17 -1
- package/dist/types/permission.d.ts +3 -3
- package/dist/types/plugin.d.ts +3 -3
- package/dist/types/provider-runner.d.ts +2 -2
- package/dist/types/provider.d.ts +10 -0
- package/dist/types/run-env.d.ts +32 -0
- package/dist/types/session.d.ts +3 -0
- package/dist/types/slash-command.d.ts +2 -2
- package/dist/types/token-counter.d.ts +30 -1
- package/dist/types/tool-executor.d.ts +2 -2
- package/dist/types/tool.d.ts +5 -5
- package/dist/utils/context-breakdown.d.ts +2 -2
- package/dist/utils/context-evidence.d.ts +12 -12
- package/dist/utils/crash-shield.d.ts +9 -0
- package/dist/utils/heap-watchdog.js +11 -3
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +72 -156
- package/dist/utils/regex-guard.d.ts +7 -30
- package/dist/utils/todos-format.d.ts +1 -1
- package/dist/utils/tree-kill.d.ts +2 -0
- package/dist/utils/tree-kill.js +1 -0
- package/instructions/coordination/subagent-baseline.md +10 -0
- package/instructions/system-lite.md +2 -0
- package/instructions/system-pro.md +40 -0
- package/instructions/system.md +15 -0
- package/package.json +6 -9
- package/dist/defaults/index.d.ts +0 -69
- package/dist/defaults/index.js +0 -38133
- /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
- /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
- /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
- /package/dist/{session-registry.d.ts → session-catalog/session-registry.d.ts} +0 -0
package/dist/types/tool.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AgentContext } from './context.js';
|
|
2
2
|
export type Permission = 'auto' | 'confirm' | 'deny';
|
|
3
3
|
/**
|
|
4
4
|
* Risk tier for tools in UI/audit surfaces. YOLO auto-approves non-denied
|
|
@@ -150,7 +150,7 @@ export interface Tool<I = unknown, O = unknown> {
|
|
|
150
150
|
* Each UI maps this id to its own icon library.
|
|
151
151
|
*/
|
|
152
152
|
icon?: ToolIconId | undefined;
|
|
153
|
-
execute(input: I, ctx:
|
|
153
|
+
execute(input: I, ctx: AgentContext, opts: {
|
|
154
154
|
signal: AbortSignal;
|
|
155
155
|
}): Promise<O>;
|
|
156
156
|
/**
|
|
@@ -176,7 +176,7 @@ export interface Tool<I = unknown, O = unknown> {
|
|
|
176
176
|
* have intermediate state shouldn't implement this; the default `execute`
|
|
177
177
|
* path is more efficient.
|
|
178
178
|
*/
|
|
179
|
-
executeStream?(input: I, ctx:
|
|
179
|
+
executeStream?(input: I, ctx: AgentContext, opts: {
|
|
180
180
|
signal: AbortSignal;
|
|
181
181
|
}): AsyncIterable<ToolStreamEvent<O>>;
|
|
182
182
|
/**
|
|
@@ -222,7 +222,7 @@ export interface Tool<I = unknown, O = unknown> {
|
|
|
222
222
|
* Avoid double-free by gating one on the other's effect, or pick a single
|
|
223
223
|
* teardown channel per resource.
|
|
224
224
|
*/
|
|
225
|
-
cleanup?(input: I, ctx:
|
|
225
|
+
cleanup?(input: I, ctx: AgentContext): Promise<void>;
|
|
226
226
|
/**
|
|
227
227
|
* Optional custom output serializer. When present, the executor's output
|
|
228
228
|
* serializer calls this INSTEAD of the central `renderToolObject()` switch
|
|
@@ -243,7 +243,7 @@ export interface ToolCallContext {
|
|
|
243
243
|
tool: Tool;
|
|
244
244
|
input: unknown;
|
|
245
245
|
callId: string;
|
|
246
|
-
ctx:
|
|
246
|
+
ctx: AgentContext;
|
|
247
247
|
signal: AbortSignal;
|
|
248
248
|
}
|
|
249
249
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AgentContext } from '../types/context.js';
|
|
2
2
|
import { type SystemBlockSource } from '../core/system-prompt-builder.js';
|
|
3
3
|
/**
|
|
4
4
|
* Real, per-category token accounting for the live context window — the data
|
|
@@ -61,5 +61,5 @@ export interface ContextBreakdown {
|
|
|
61
61
|
* interactively (e.g. from `/context`): it walks the full message array once, so
|
|
62
62
|
* it is O(messages·blocks), not the O(1) cached path the per-turn bar uses.
|
|
63
63
|
*/
|
|
64
|
-
export declare function getContextBreakdown(ctx:
|
|
64
|
+
export declare function getContextBreakdown(ctx: AgentContext): ContextBreakdown;
|
|
65
65
|
//# sourceMappingURL=context-breakdown.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AgentContext } from '../types/context.js';
|
|
2
2
|
import { type TextBlock } from '../types/blocks.js';
|
|
3
3
|
import type { CompactReport } from '../types/compactor.js';
|
|
4
4
|
import type { CompletedWorkEvidence, CompletedWorkSource, ContextEvidenceState, ToolOutputMetadata } from '../types/context-evidence.js';
|
|
@@ -13,7 +13,7 @@ export interface RecordToolOutputEvidenceInput {
|
|
|
13
13
|
outputTokens?: number | undefined;
|
|
14
14
|
outputLines?: number | undefined;
|
|
15
15
|
}
|
|
16
|
-
export declare function recordUserIntentEvidence(ctx:
|
|
16
|
+
export declare function recordUserIntentEvidence(ctx: AgentContext, text: string): void;
|
|
17
17
|
/** Runtime steering/status blocks must not displace real conversation turns. */
|
|
18
18
|
export declare function isRuntimeContextInput(text: string): boolean;
|
|
19
19
|
/**
|
|
@@ -25,11 +25,11 @@ export declare function isRuntimeContextInput(text: string): boolean;
|
|
|
25
25
|
* It is volatile provider evidence, so it never grows the durable chat log or
|
|
26
26
|
* invalidates the cached base prompt.
|
|
27
27
|
*/
|
|
28
|
-
export declare function buildConversationContinuityBlock(ctx: Pick<
|
|
29
|
-
export declare function recordToolOutputEvidence(ctx:
|
|
30
|
-
export declare function markAssistantReferencedEvidence(ctx:
|
|
31
|
-
export declare function buildContextEvidenceDigest(ctx:
|
|
32
|
-
export declare function repeatedReadPressure(ctx:
|
|
28
|
+
export declare function buildConversationContinuityBlock(ctx: Pick<AgentContext, 'contextEvidence' | 'messages'>): TextBlock | undefined;
|
|
29
|
+
export declare function recordToolOutputEvidence(ctx: AgentContext, input: RecordToolOutputEvidenceInput): ToolOutputMetadata;
|
|
30
|
+
export declare function markAssistantReferencedEvidence(ctx: AgentContext, text: string): void;
|
|
31
|
+
export declare function buildContextEvidenceDigest(ctx: AgentContext): string;
|
|
32
|
+
export declare function repeatedReadPressure(ctx: AgentContext): number;
|
|
33
33
|
/**
|
|
34
34
|
* Deterministic post-compaction sanity check. Cheap and local: records whether
|
|
35
35
|
* the compacted context still carries an intent anchor and a tool/path trail.
|
|
@@ -37,7 +37,7 @@ export declare function repeatedReadPressure(ctx: Context): number;
|
|
|
37
37
|
* only HybridCompactor did). Advisory on its own — pair with
|
|
38
38
|
* `injectEvidenceFloor` to actually repair a flagged loss.
|
|
39
39
|
*/
|
|
40
|
-
export declare function checkCompactionQuality(ctx:
|
|
40
|
+
export declare function checkCompactionQuality(ctx: AgentContext, opts: {
|
|
41
41
|
collapsedDigest?: string | undefined;
|
|
42
42
|
evidenceDigest?: string | undefined;
|
|
43
43
|
reduced: boolean;
|
|
@@ -50,7 +50,7 @@ export declare function checkCompactionQuality(ctx: Context, opts: {
|
|
|
50
50
|
* no-op when quality is fine or there is no evidence to inject. Returns true
|
|
51
51
|
* when it injected a block (so the caller re-estimates tokens).
|
|
52
52
|
*/
|
|
53
|
-
export declare function injectEvidenceFloor(ctx:
|
|
53
|
+
export declare function injectEvidenceFloor(ctx: AgentContext, quality: CompactReport['quality'] | undefined): boolean;
|
|
54
54
|
/** Marker prefixing the ledger's system-prompt block (also used to find/replace it). */
|
|
55
55
|
export declare const COMPLETED_WORK_LEDGER_MARKER = "[completed_work_ledger]";
|
|
56
56
|
export interface RecordCompletedWorkInput {
|
|
@@ -68,14 +68,14 @@ export interface RecordCompletedWorkInput {
|
|
|
68
68
|
* composer renders this state as a volatile tail block; the stable system-prompt
|
|
69
69
|
* prefix is never mutated here.
|
|
70
70
|
*/
|
|
71
|
-
export declare function recordCompletedWorkEvidence(ctx:
|
|
71
|
+
export declare function recordCompletedWorkEvidence(ctx: AgentContext, input: RecordCompletedWorkInput): CompletedWorkEvidence;
|
|
72
72
|
/** Render the ledger's system-prompt block text (marker + newest entries). */
|
|
73
73
|
export declare function formatCompletedWorkLedger(items: readonly CompletedWorkEvidence[]): string;
|
|
74
74
|
/** Build the current volatile completed-work block without mutating the prompt. */
|
|
75
|
-
export declare function buildCompletedWorkLedgerBlock(ctx:
|
|
75
|
+
export declare function buildCompletedWorkLedgerBlock(ctx: AgentContext): TextBlock | undefined;
|
|
76
76
|
/**
|
|
77
77
|
* @deprecated Volatile state must be composed at request time. Kept as a
|
|
78
78
|
* compatibility no-op for embedders importing the old helper.
|
|
79
79
|
*/
|
|
80
|
-
export declare function syncCompletedWorkLedgerBlock(_ctx:
|
|
80
|
+
export declare function syncCompletedWorkLedgerBlock(_ctx: AgentContext): void;
|
|
81
81
|
//# sourceMappingURL=context-evidence.d.ts.map
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/** Register a synchronous salvage hook. Returns an unregister function. */
|
|
2
|
+
export declare function addFatalSalvageHook(hook: () => void): () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Run every registered salvage hook once. Swallows individual hook failures
|
|
5
|
+
* so one broken hook cannot skip the remaining ones. Safe to call multiple
|
|
6
|
+
* times — hooks are expected to be idempotent (a drained buffer stays
|
|
7
|
+
* drained).
|
|
8
|
+
*/
|
|
9
|
+
export declare function runFatalSalvageSync(): void;
|
|
1
10
|
interface CrashShieldTarget {
|
|
2
11
|
on(event: 'unhandledRejection' | 'uncaughtException', listener: (reason: unknown) => void): unknown;
|
|
3
12
|
off(event: 'unhandledRejection' | 'uncaughtException', listener: (reason: unknown) => void): unknown;
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as fsp2 from "node:fs/promises";
|
|
4
4
|
import * as path3 from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
PerformanceObserver,
|
|
7
|
+
constants as performanceConstants
|
|
8
|
+
} from "node:perf_hooks";
|
|
6
9
|
import * as v8 from "node:v8";
|
|
7
10
|
|
|
8
11
|
// src/utils/memory-flight-recorder.ts
|
|
@@ -308,7 +311,8 @@ function createMemoryFlightRecorder(options = {}) {
|
|
|
308
311
|
const now = options.now ?? Date.now;
|
|
309
312
|
const startedAt = now();
|
|
310
313
|
const root = options.diagnosticsRoot ?? path2.join(wstackGlobalRoot(), "diagnostics", "memory");
|
|
311
|
-
const
|
|
314
|
+
const isBunRuntime = typeof globalThis.Bun !== "undefined";
|
|
315
|
+
const enabled = options.enabled ?? (!isBunRuntime && process.env["WRONGSTACK_MEMORY_FLIGHT_RECORDER"] !== "0" && process.env["NODE_ENV"] !== "test");
|
|
312
316
|
const writer = options.captureWriter ?? (enabled ? new ContinuousMemoryCaptureWriter(root) : disabledWriter(path2.join(root, `${timestampSlug()}-pid-${process.pid}`)));
|
|
313
317
|
const observations = [];
|
|
314
318
|
const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
|
|
@@ -418,7 +422,11 @@ function defaultHeapLogPath() {
|
|
|
418
422
|
function takeHeapSample() {
|
|
419
423
|
const m = process.memoryUsage();
|
|
420
424
|
const heap = v8.getHeapStatistics();
|
|
421
|
-
|
|
425
|
+
let spaces = [];
|
|
426
|
+
try {
|
|
427
|
+
spaces = v8.getHeapSpaceStatistics();
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
422
430
|
const spaceUsed = (name) => spaces.find((space) => space.space_name === name)?.space_used_size ?? 0;
|
|
423
431
|
const limit = heap.heap_size_limit || 0;
|
|
424
432
|
let activeResources = 0;
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export * from './config-backup.js';
|
|
|
7
7
|
export * from './config-json.js';
|
|
8
8
|
export { checkConnectivity, resetConnectivityCache } from './connectivity.js';
|
|
9
9
|
export { type ContextBreakdown, getContextBreakdown } from './context-breakdown.js';
|
|
10
|
-
export { type CrashShieldOptions, installCrashShield } from './crash-shield.js';
|
|
10
|
+
export { addFatalSalvageHook, runFatalSalvageSync, type CrashShieldOptions, installCrashShield, } from './crash-shield.js';
|
|
11
11
|
export { buildCompletedWorkLedgerBlock, buildContextEvidenceDigest, COMPLETED_WORK_LEDGER_MARKER, createContextEvidenceState, formatCompletedWorkLedger, markAssistantReferencedEvidence, type RecordCompletedWorkInput, type RecordToolOutputEvidenceInput, recordCompletedWorkEvidence, recordToolOutputEvidence, recordUserIntentEvidence, repeatedReadPressure, syncCompletedWorkLedgerBlock, } from './context-evidence.js';
|
|
12
12
|
export { type DeepMergeOptions, deepMerge, FORBIDDEN_PROTO_KEYS, isPrimitiveArray, } from './deep-merge.js';
|
|
13
13
|
export * from './diff.js';
|
package/dist/utils/index.js
CHANGED
|
@@ -1915,6 +1915,22 @@ function getContextBreakdown(ctx) {
|
|
|
1915
1915
|
// src/utils/crash-shield.ts
|
|
1916
1916
|
var CRASH_STORM_WINDOW_MS = 6e4;
|
|
1917
1917
|
var CRASH_STORM_LIMIT = 20;
|
|
1918
|
+
var fatalSalvageHooks = [];
|
|
1919
|
+
function addFatalSalvageHook(hook) {
|
|
1920
|
+
fatalSalvageHooks.push(hook);
|
|
1921
|
+
return () => {
|
|
1922
|
+
const idx = fatalSalvageHooks.indexOf(hook);
|
|
1923
|
+
if (idx >= 0) fatalSalvageHooks.splice(idx, 1);
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
function runFatalSalvageSync() {
|
|
1927
|
+
for (const hook of [...fatalSalvageHooks]) {
|
|
1928
|
+
try {
|
|
1929
|
+
hook();
|
|
1930
|
+
} catch {
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1918
1934
|
function isBrokenOutputConsumer(error) {
|
|
1919
1935
|
if (typeof error !== "object" || error === null || !("code" in error)) return false;
|
|
1920
1936
|
return error.code === "EPIPE" || error.code === "ECONNRESET" || error.code === "ECONNABORTED";
|
|
@@ -1950,6 +1966,7 @@ function installCrashShield(options = {}) {
|
|
|
1950
1966
|
`error storm: >${CRASH_STORM_LIMIT} in ${CRASH_STORM_WINDOW_MS / 1e3}s \u2014 exiting`
|
|
1951
1967
|
)
|
|
1952
1968
|
);
|
|
1969
|
+
runFatalSalvageSync();
|
|
1953
1970
|
exit(1);
|
|
1954
1971
|
}
|
|
1955
1972
|
};
|
|
@@ -2232,49 +2249,57 @@ async function expandGlob(pattern) {
|
|
|
2232
2249
|
async function walk2(dir, pat) {
|
|
2233
2250
|
let entries;
|
|
2234
2251
|
try {
|
|
2235
|
-
entries = await fsp.readdir(dir);
|
|
2252
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
2236
2253
|
} catch {
|
|
2237
2254
|
return;
|
|
2238
2255
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2256
|
+
if (pat.startsWith("**/")) {
|
|
2257
|
+
const rest = pat.slice(3);
|
|
2258
|
+
await walk2(dir, rest);
|
|
2242
2259
|
for (const e of entries) {
|
|
2243
|
-
if (
|
|
2244
|
-
const
|
|
2245
|
-
|
|
2260
|
+
if (e.isDirectory()) {
|
|
2261
|
+
const subDir = `${dir}${SEP}${e.name}`;
|
|
2262
|
+
await walk2(subDir, pat);
|
|
2246
2263
|
}
|
|
2247
2264
|
}
|
|
2248
2265
|
return;
|
|
2249
2266
|
}
|
|
2250
|
-
|
|
2251
|
-
const rest = pat.slice(firstGlob);
|
|
2252
|
-
if (before.endsWith("**")) {
|
|
2253
|
-
await walk2(dir, rest);
|
|
2267
|
+
if (pat === "**") {
|
|
2254
2268
|
for (const e of entries) {
|
|
2255
|
-
const full = `${dir}${SEP}${e}`;
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
} catch {
|
|
2269
|
+
const full = `${dir}${SEP}${e.name}`;
|
|
2270
|
+
results.add(abs ? resolve2(full) : full);
|
|
2271
|
+
if (e.isDirectory()) {
|
|
2272
|
+
await walk2(full, "**");
|
|
2260
2273
|
}
|
|
2261
2274
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
const firstSlash = pat.indexOf("/");
|
|
2278
|
+
if (firstSlash < 0) {
|
|
2279
|
+
const re = globToRegex(pat);
|
|
2264
2280
|
for (const e of entries) {
|
|
2265
|
-
if (re.test(e)) {
|
|
2266
|
-
const full = `${dir}${SEP}${e}`;
|
|
2281
|
+
if (re.test(e.name)) {
|
|
2282
|
+
const full = `${dir}${SEP}${e.name}`;
|
|
2267
2283
|
results.add(abs ? resolve2(full) : full);
|
|
2268
2284
|
}
|
|
2269
2285
|
}
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
const currentSeg = pat.slice(0, firstSlash);
|
|
2289
|
+
const remainingPat = pat.slice(firstSlash + 1);
|
|
2290
|
+
if (isGlob(currentSeg)) {
|
|
2291
|
+
const re = globToRegex(currentSeg);
|
|
2292
|
+
for (const e of entries) {
|
|
2293
|
+
if (e.isDirectory() && re.test(e.name)) {
|
|
2294
|
+
const subDir = `${dir}${SEP}${e.name}`;
|
|
2295
|
+
await walk2(subDir, remainingPat);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2270
2298
|
} else {
|
|
2271
|
-
const
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
const stat4 = await fsp.stat(full);
|
|
2276
|
-
if (stat4.isDirectory()) await walk2(full, rest);
|
|
2277
|
-
} catch {
|
|
2299
|
+
for (const e of entries) {
|
|
2300
|
+
if (e.isDirectory() && e.name === currentSeg) {
|
|
2301
|
+
const subDir = `${dir}${SEP}${e.name}`;
|
|
2302
|
+
await walk2(subDir, remainingPat);
|
|
2278
2303
|
}
|
|
2279
2304
|
}
|
|
2280
2305
|
}
|
|
@@ -2379,7 +2404,10 @@ function matchAnyCommand(patterns, input) {
|
|
|
2379
2404
|
import * as fs4 from "node:fs";
|
|
2380
2405
|
import * as fsp3 from "node:fs/promises";
|
|
2381
2406
|
import * as path5 from "node:path";
|
|
2382
|
-
import {
|
|
2407
|
+
import {
|
|
2408
|
+
PerformanceObserver,
|
|
2409
|
+
constants as performanceConstants
|
|
2410
|
+
} from "node:perf_hooks";
|
|
2383
2411
|
import * as v8 from "node:v8";
|
|
2384
2412
|
|
|
2385
2413
|
// src/utils/memory-flight-recorder.ts
|
|
@@ -2696,8 +2724,8 @@ async function pruneOldSessions(root, currentDir) {
|
|
|
2696
2724
|
const sessions = await Promise.all(
|
|
2697
2725
|
entries.filter((entry) => entry.isDirectory() && entry.name !== path4.basename(currentDir)).map(async (entry) => {
|
|
2698
2726
|
const fullPath = path4.join(root, entry.name);
|
|
2699
|
-
const
|
|
2700
|
-
return { fullPath, mtimeMs:
|
|
2727
|
+
const stat3 = await fsp2.stat(fullPath);
|
|
2728
|
+
return { fullPath, mtimeMs: stat3.mtimeMs };
|
|
2701
2729
|
})
|
|
2702
2730
|
);
|
|
2703
2731
|
sessions.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -2815,7 +2843,8 @@ function createMemoryFlightRecorder(options = {}) {
|
|
|
2815
2843
|
const now = options.now ?? Date.now;
|
|
2816
2844
|
const startedAt = now();
|
|
2817
2845
|
const root = options.diagnosticsRoot ?? path4.join(wstackGlobalRoot(), "diagnostics", "memory");
|
|
2818
|
-
const
|
|
2846
|
+
const isBunRuntime = typeof globalThis.Bun !== "undefined";
|
|
2847
|
+
const enabled = options.enabled ?? (!isBunRuntime && process.env["WRONGSTACK_MEMORY_FLIGHT_RECORDER"] !== "0" && process.env["NODE_ENV"] !== "test");
|
|
2819
2848
|
const writer = options.captureWriter ?? (enabled ? new ContinuousMemoryCaptureWriter(root) : disabledWriter(path4.join(root, `${timestampSlug()}-pid-${process.pid}`)));
|
|
2820
2849
|
const observations = [];
|
|
2821
2850
|
const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
|
|
@@ -2925,7 +2954,11 @@ function defaultHeapLogPath() {
|
|
|
2925
2954
|
function takeHeapSample() {
|
|
2926
2955
|
const m = process.memoryUsage();
|
|
2927
2956
|
const heap = v8.getHeapStatistics();
|
|
2928
|
-
|
|
2957
|
+
let spaces = [];
|
|
2958
|
+
try {
|
|
2959
|
+
spaces = v8.getHeapSpaceStatistics();
|
|
2960
|
+
} catch {
|
|
2961
|
+
}
|
|
2929
2962
|
const spaceUsed = (name) => spaces.find((space) => space.space_name === name)?.space_used_size ?? 0;
|
|
2930
2963
|
const limit = heap.heap_size_limit || 0;
|
|
2931
2964
|
let activeResources = 0;
|
|
@@ -4270,130 +4303,11 @@ function watchProjectTree(root, listener, opts) {
|
|
|
4270
4303
|
}
|
|
4271
4304
|
|
|
4272
4305
|
// src/utils/regex-guard.ts
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
// Adjacent quantifiers: a++ a*+
|
|
4279
|
-
/[+*]{2,}/,
|
|
4280
|
-
// Quantifier on alternation with length 2+
|
|
4281
|
-
/\([^|)]+\|[^)]+\)[+*][+*]/,
|
|
4282
|
-
// Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
|
|
4283
|
-
/[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
|
|
4284
|
-
];
|
|
4285
|
-
function hasAmbiguousQuantifiedAlternation(pattern) {
|
|
4286
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
4287
|
-
if (pattern[i] !== "(") continue;
|
|
4288
|
-
if (i > 0 && pattern[i - 1] === "\\") continue;
|
|
4289
|
-
let depth = 0;
|
|
4290
|
-
let inClass = false;
|
|
4291
|
-
let j = i;
|
|
4292
|
-
for (; j < pattern.length; j++) {
|
|
4293
|
-
const ch = pattern[j];
|
|
4294
|
-
if (ch === "\\") {
|
|
4295
|
-
j++;
|
|
4296
|
-
continue;
|
|
4297
|
-
}
|
|
4298
|
-
if (inClass) {
|
|
4299
|
-
if (ch === "]") inClass = false;
|
|
4300
|
-
continue;
|
|
4301
|
-
}
|
|
4302
|
-
if (ch === "[") {
|
|
4303
|
-
inClass = true;
|
|
4304
|
-
continue;
|
|
4305
|
-
}
|
|
4306
|
-
if (ch === "(") depth++;
|
|
4307
|
-
else if (ch === ")") {
|
|
4308
|
-
depth--;
|
|
4309
|
-
if (depth === 0) break;
|
|
4310
|
-
}
|
|
4311
|
-
}
|
|
4312
|
-
if (j >= pattern.length) return false;
|
|
4313
|
-
const next = pattern[j + 1];
|
|
4314
|
-
if (next !== "+" && next !== "*" && next !== "{") continue;
|
|
4315
|
-
let inner = pattern.slice(i + 1, j);
|
|
4316
|
-
inner = inner.replace(/^\?(?::|<?[=!])/u, "");
|
|
4317
|
-
const branches = [];
|
|
4318
|
-
let current = "";
|
|
4319
|
-
let d = 0;
|
|
4320
|
-
let cls = false;
|
|
4321
|
-
for (let k = 0; k < inner.length; k++) {
|
|
4322
|
-
const ch = inner[k];
|
|
4323
|
-
if (ch === "\\") {
|
|
4324
|
-
current += ch + (inner[k + 1] ?? "");
|
|
4325
|
-
k++;
|
|
4326
|
-
continue;
|
|
4327
|
-
}
|
|
4328
|
-
if (cls) {
|
|
4329
|
-
if (ch === "]") cls = false;
|
|
4330
|
-
current += ch;
|
|
4331
|
-
continue;
|
|
4332
|
-
}
|
|
4333
|
-
if (ch === "[") {
|
|
4334
|
-
cls = true;
|
|
4335
|
-
current += ch;
|
|
4336
|
-
continue;
|
|
4337
|
-
}
|
|
4338
|
-
if (ch === "(") d++;
|
|
4339
|
-
if (ch === ")") d--;
|
|
4340
|
-
if (ch === "|" && d === 0) {
|
|
4341
|
-
branches.push(current);
|
|
4342
|
-
current = "";
|
|
4343
|
-
continue;
|
|
4344
|
-
}
|
|
4345
|
-
current += ch;
|
|
4346
|
-
}
|
|
4347
|
-
branches.push(current);
|
|
4348
|
-
if (branches.length < 2) continue;
|
|
4349
|
-
for (let a = 0; a < branches.length; a++) {
|
|
4350
|
-
for (let b = a + 1; b < branches.length; b++) {
|
|
4351
|
-
const x = branches[a];
|
|
4352
|
-
const y = branches[b];
|
|
4353
|
-
if (x === "" || y === "") return true;
|
|
4354
|
-
if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
|
|
4355
|
-
}
|
|
4356
|
-
}
|
|
4357
|
-
}
|
|
4358
|
-
return false;
|
|
4359
|
-
}
|
|
4360
|
-
function compileUserRegex(pattern, flags) {
|
|
4361
|
-
if (typeof pattern !== "string") {
|
|
4362
|
-
return { ok: false, reason: "pattern must be a string" };
|
|
4363
|
-
}
|
|
4364
|
-
if (pattern.length === 0) {
|
|
4365
|
-
return { ok: false, reason: "pattern is empty" };
|
|
4366
|
-
}
|
|
4367
|
-
if (pattern.length > MAX_PATTERN_LEN) {
|
|
4368
|
-
return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
|
|
4369
|
-
}
|
|
4370
|
-
for (const rx of DANGEROUS_PATTERNS) {
|
|
4371
|
-
if (rx.test(pattern)) {
|
|
4372
|
-
return {
|
|
4373
|
-
ok: false,
|
|
4374
|
-
reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
|
|
4375
|
-
};
|
|
4376
|
-
}
|
|
4377
|
-
}
|
|
4378
|
-
if (hasAmbiguousQuantifiedAlternation(pattern)) {
|
|
4379
|
-
return {
|
|
4380
|
-
ok: false,
|
|
4381
|
-
reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
|
|
4382
|
-
};
|
|
4383
|
-
}
|
|
4384
|
-
try {
|
|
4385
|
-
return { ok: true, regex: new RegExp(pattern, flags) };
|
|
4386
|
-
} catch (err) {
|
|
4387
|
-
return {
|
|
4388
|
-
ok: false,
|
|
4389
|
-
reason: err instanceof Error ? err.message : "invalid regex"
|
|
4390
|
-
};
|
|
4391
|
-
}
|
|
4392
|
-
}
|
|
4393
|
-
var MAX_SUBJECT_LEN = 64 * 1024;
|
|
4394
|
-
function capSubject(line) {
|
|
4395
|
-
return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;
|
|
4396
|
-
}
|
|
4306
|
+
import {
|
|
4307
|
+
capSubject,
|
|
4308
|
+
compileUserRegex,
|
|
4309
|
+
MAX_SUBJECT_LEN
|
|
4310
|
+
} from "@wrongstack/primitives";
|
|
4397
4311
|
|
|
4398
4312
|
// src/utils/safe-json.ts
|
|
4399
4313
|
function safeParse(input, maxBytes = 5e6) {
|
|
@@ -5799,6 +5713,7 @@ export {
|
|
|
5799
5713
|
WIRE_TOOL_NAME_MAX_LENGTH,
|
|
5800
5714
|
WIRE_TOOL_NAME_PATTERN,
|
|
5801
5715
|
activateProjectStateGuard,
|
|
5716
|
+
addFatalSalvageHook,
|
|
5802
5717
|
applyToolDescriptionModeToTool,
|
|
5803
5718
|
applyToolDescriptionModes,
|
|
5804
5719
|
applyToolResultRenderModes,
|
|
@@ -5933,6 +5848,7 @@ export {
|
|
|
5933
5848
|
resolveToolDescriptionMode,
|
|
5934
5849
|
resolveToolResultRenderMode,
|
|
5935
5850
|
resolveWstackPaths,
|
|
5851
|
+
runFatalSalvageSync,
|
|
5936
5852
|
safeEmit,
|
|
5937
5853
|
safeParse,
|
|
5938
5854
|
safeProfileName,
|
|
@@ -1,35 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compile a user-supplied regex with conservative bounds against ReDoS.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* subject cap at all. `packages/tools/tests/regex-guard-parity.test.ts` now
|
|
11
|
-
* holds all three to the same verdicts — change one, change all three.
|
|
12
|
-
*
|
|
13
|
-
* V8's regex engine is backtracking-based and cannot interrupt a
|
|
14
|
-
* synchronous match — a pattern like `(a+)+$` against a sufficiently
|
|
15
|
-
* long line will pin a worker for seconds.
|
|
16
|
-
*/
|
|
17
|
-
export interface CompileResult {
|
|
18
|
-
ok: true;
|
|
19
|
-
regex: RegExp;
|
|
20
|
-
}
|
|
21
|
-
export interface CompileFail {
|
|
22
|
-
ok: false;
|
|
23
|
-
reason: string;
|
|
24
|
-
}
|
|
25
|
-
export declare function compileUserRegex(pattern: string, flags: string): CompileResult | CompileFail;
|
|
26
|
-
/**
|
|
27
|
-
* Truncate a subject line to a safe length for synchronous regex eval.
|
|
28
|
-
*
|
|
29
|
-
* The canonical copy has always shipped this; this one did not, so callers
|
|
30
|
-
* here bounded the pattern but never the SUBJECT — and a linear-time pattern
|
|
31
|
-
* over a multi-megabyte line is still a stall.
|
|
4
|
+
* Canonical home since card #5: `@wrongstack/primitives/src/regex-guard.ts`
|
|
5
|
+
* (a dependency leaf, so every workspace tier — including kanban, which sits
|
|
6
|
+
* below core and tools — can share one implementation). This module is now a
|
|
7
|
+
* re-export shim: importers of `@wrongstack/core/utils` keep working, and
|
|
8
|
+
* `tools/tests/regex-guard-parity.test.ts` still pins all three historical
|
|
9
|
+
* entry points to identical verdicts (now trivially true — one code path).
|
|
32
10
|
*/
|
|
33
|
-
export
|
|
34
|
-
export declare function capSubject(line: string): string;
|
|
11
|
+
export { type CompileFail, type CompileResult, capSubject, compileUserRegex, MAX_SUBJECT_LEN, } from '@wrongstack/primitives';
|
|
35
12
|
//# sourceMappingURL=regex-guard.d.ts.map
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export interface KillableChild {
|
|
7
7
|
readonly pid?: number | undefined;
|
|
8
|
+
/** Non-null once Node has observed that the child exited. */
|
|
9
|
+
readonly exitCode?: number | null | undefined;
|
|
8
10
|
kill(signal?: NodeJS.Signals | number): boolean | void;
|
|
9
11
|
}
|
|
10
12
|
export interface TreeKillOptions {
|
package/dist/utils/tree-kill.js
CHANGED
|
@@ -20,6 +20,16 @@ self-contained handoff; do not take over fleet orchestration.
|
|
|
20
20
|
provides, and write it yourself only when none of them fit — then write the
|
|
21
21
|
smallest version. A new dependency needs an explicit grant. The ladder trims
|
|
22
22
|
code you invented; it never shrinks the assigned deliverable.
|
|
23
|
+
- Shape new code by architectural discipline, applied on trigger rather than as
|
|
24
|
+
ceremony: domain logic stays free of framework/SDK/I/O imports (external
|
|
25
|
+
services behind adapters at the edge, dependencies pointing inward); program
|
|
26
|
+
to interfaces when a behavior has or will have multiple implementations;
|
|
27
|
+
factories create multi-provider/dynamic services (payments, AI models,
|
|
28
|
+
storage); singletons only for expensive shared resources (pools, loggers,
|
|
29
|
+
cache); strategies replace `if`/`switch` over more than two interchangeable
|
|
30
|
+
behaviors; typed events decouple side-effects (audit, email, cache
|
|
31
|
+
invalidation). Watch SRP past ~200 lines. Name the pattern applied in one
|
|
32
|
+
line when scaffolding.
|
|
23
33
|
<!--ws:if tool=codebase-search-->
|
|
24
34
|
Confirm the repo does not already do it with `codebase-search` before writing
|
|
25
35
|
a new helper.
|
|
@@ -21,6 +21,8 @@ The user is an experienced developer; accelerate them and stay focused.
|
|
|
21
21
|
12. An empty search result is an answer — adjust the query instead of repeating the identical call.
|
|
22
22
|
13. Keep responses concise and scannable.
|
|
23
23
|
14. Match the user's language.
|
|
24
|
+
15. Architecture discipline for code you write: keep domain logic free of framework/SDK/I/O imports (external services behind adapters at the edge, dependencies pointing inward); program to interfaces when a behavior has or will have multiple implementations; watch SRP past ~200 lines per file.
|
|
25
|
+
16. Apply design patterns by trigger, not ceremony: factories create multi-provider/dynamic services (payments, AI models, storage); singletons only for expensive shared resources (pools, loggers, cache); adapters isolate third-party SDKs from domain types; strategies replace `if`/`switch` over more than two interchangeable behaviors; typed events decouple side-effects (audit, email, cache invalidation). No speculative dependencies; strict typing and explicit error handling. When scaffolding, name the pattern applied in one line.
|
|
24
26
|
|
|
25
27
|
## Working loop
|
|
26
28
|
|
|
@@ -138,6 +138,46 @@ The five questions above decide *whether the change is right*. This ladder decid
|
|
|
138
138
|
|
|
139
139
|
**Guardrails.** The ladder trims what **you** invented; it never shrinks what the user asked for — rung 1 is not a license 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.
|
|
140
140
|
|
|
141
|
+
## Architecture discipline
|
|
142
|
+
|
|
143
|
+
The reasoning protocol decides *whether* the change is right; the cost ladder decides *how much* it costs; this section decides *what shape* the code takes. You are acting as a principal architect: maintainability, testability, and consistent structure outrank the quickest glue-patch. These rules govern only the code you write or touch — they are never a license to refactor working neighbors; note violations in your summary instead.
|
|
144
|
+
|
|
145
|
+
### Layering (Clean/Hexagonal boundaries)
|
|
146
|
+
|
|
147
|
+
- **Domain/Core:** pure business logic — zero framework, SDK, or I/O imports. It does not know a database or a vendor exists.
|
|
148
|
+
- **Application/Use-cases:** orchestration. Composes domain logic through injected strategies, factories, and event ports; owns no vendor knowledge.
|
|
149
|
+
- **Infrastructure/Adapters:** DB drivers, third-party APIs, HTTP/RPC servers, queues — everything at the edge, wrapped behind domain-owned interfaces.
|
|
150
|
+
- Dependencies always point inward. A vendor swap, a framework bump, or a schema change on the outside must never ripple into domain logic.
|
|
151
|
+
|
|
152
|
+
### Structural rules
|
|
153
|
+
|
|
154
|
+
- **Program to interfaces, not implementations.** Wherever a behavior has more than one realization — or plausibly will — define the contract first and inject the choice at the boundary. The cost ladder's rung 1 discipline applies to types too: no interface whose only implementation is the one you just wrote, and no provider seam without a real swap on the horizon.
|
|
155
|
+
- **Prefer composition over inheritance.** Build behavior from injected parts rather than class hierarchies.
|
|
156
|
+
- **Single responsibility per file.** Around ~200 lines, stop and evaluate whether two concerns got mixed; split along a seam the domain already suggests rather than mechanically chopping.
|
|
157
|
+
- **Zero speculative dependencies or framework sprawl.** Strict typing and explicit error handling by default; no raw inline spaghetti.
|
|
158
|
+
|
|
159
|
+
### The five constitutional patterns
|
|
160
|
+
|
|
161
|
+
Apply when the trigger is present; skip the ceremony when it is not. Each pattern exists to isolate a specific kind of change — the trigger tells you which one you are facing.
|
|
162
|
+
|
|
163
|
+
1. **Factory (creational).** Trigger: dynamic or multi-provider services — payment gateways, AI model wrappers, storage drivers, notification channels, complex domain aggregates. Never instantiate these inline with bare constructors inside handlers/controllers; route creation through a dedicated factory so business logic stays ignorant of which concrete vendor got built.
|
|
164
|
+
2. **Singleton (creational).** Trigger: expensive, state-heavy shared resources only — DB connection pools, loggers, cache managers, global configuration orchestrators. Everything else gets a normal injected lifetime. Ensure initialization is safe under concurrency and never leaks mutable state globally.
|
|
165
|
+
3. **Adapter (structural).** Trigger: any third-party SDK, legacy system, or external API schema. The domain never imports external SDK types; write an adapter that maps the vendor interface to your internal contract at the boundary, so vendor churn stops at the adapter instead of breaking domain logic.
|
|
166
|
+
4. **Strategy (behavioral).** Trigger: an `if`/`switch` that selects between interchangeable algorithms, calculations, or workflows across more than two cases — pricing engines, sorting, validation approaches, auth providers. Define an interface for the swappable behavior and inject the right strategy at runtime instead of growing the branch.
|
|
167
|
+
5. **Observer / typed events (behavioral).** Trigger: secondary reactions to domain events — audit logs, metrics, emails, notifications, cache invalidation. Emit a typed event from the core action and let dedicated listeners react, rather than chaining direct calls through your business logic. Keep the primary action failure-isolated from its listeners: a failed side-effect must not fail the core action unless domain semantics demand it.
|
|
168
|
+
|
|
169
|
+
### Self-correction pass
|
|
170
|
+
|
|
171
|
+
Before presenting non-trivial code, ask once:
|
|
172
|
+
- *"Did I hardcode an external dependency?"* → wrap it in an adapter behind a domain-owned interface.
|
|
173
|
+
- *"Am I branching across more than two cases of interchangeable behavior?"* → refactor into an injected strategy.
|
|
174
|
+
- *"Am I constructing complex/vendored objects inside a handler?"* → delegate to a factory.
|
|
175
|
+
- *"Is a side-effect (audit, email, cache invalidation) wired directly into business logic?"* → decouple through a typed event.
|
|
176
|
+
|
|
177
|
+
### Output discipline
|
|
178
|
+
|
|
179
|
+
When scaffolding a feature or adding a component, state briefly which patterns you applied and why — one line per pattern, not a lecture. Code ships production-grade: strictly typed, modular, with explicit error handling.
|
|
180
|
+
|
|
141
181
|
<!--ws:if tool=todo-->
|
|
142
182
|
## Todo status lifecycle
|
|
143
183
|
|