@wrongstack/tools 0.293.0 → 0.295.1
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/_env.d.ts +1 -1
- package/dist/_env.d.ts.map +1 -1
- package/dist/_fetch-guard.d.ts.map +1 -1
- package/dist/_spawn-stream.d.ts +1 -1
- package/dist/_spawn-stream.d.ts.map +1 -1
- package/dist/_util.d.ts +1 -1
- package/dist/_util.d.ts.map +1 -1
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +81 -16
- package/dist/audit.js.map +2 -2
- package/dist/auto-proceed-loop-guard.d.ts +31 -0
- package/dist/auto-proceed-loop-guard.d.ts.map +1 -1
- package/dist/auto-proceed-loop-guard.js +24 -1
- package/dist/auto-proceed-loop-guard.js.map +2 -2
- package/dist/bash.d.ts +1 -1
- package/dist/bash.d.ts.map +1 -1
- package/dist/bash.js +105 -75
- package/dist/bash.js.map +3 -3
- package/dist/batch-tool-use.d.ts +1 -1
- package/dist/batch-tool-use.d.ts.map +1 -1
- package/dist/batch-tool-use.js +1 -1
- package/dist/batch-tool-use.js.map +1 -1
- package/dist/browser/index.js +10 -1
- package/dist/browser/index.js.map +2 -2
- package/dist/browser/manager.d.ts +2 -0
- package/dist/browser/manager.d.ts.map +1 -1
- package/dist/browser/tools.d.ts +1 -1
- package/dist/browser/tools.d.ts.map +1 -1
- package/dist/builtin.d.ts +1 -1
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +1650 -640
- package/dist/builtin.js.map +4 -4
- package/dist/codebase-index/background-indexer.d.ts +2 -2
- package/dist/codebase-index/background-indexer.d.ts.map +1 -1
- package/dist/codebase-index/bm25.d.ts +3 -1
- package/dist/codebase-index/bm25.d.ts.map +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
- package/dist/codebase-index/generic-parser.d.ts +34 -0
- package/dist/codebase-index/generic-parser.d.ts.map +1 -0
- package/dist/codebase-index/go-parser.d.ts +1 -1
- package/dist/codebase-index/go-parser.d.ts.map +1 -1
- package/dist/codebase-index/index.d.ts +4 -2
- package/dist/codebase-index/index.d.ts.map +1 -1
- package/dist/codebase-index/index.js +1068 -372
- package/dist/codebase-index/index.js.map +4 -4
- package/dist/codebase-index/indexer.d.ts +6 -1
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/json-parser.d.ts +1 -1
- package/dist/codebase-index/languages.d.ts +25 -0
- package/dist/codebase-index/languages.d.ts.map +1 -0
- package/dist/codebase-index/py-parser.d.ts +9 -1
- package/dist/codebase-index/py-parser.d.ts.map +1 -1
- package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
- package/dist/codebase-index/rs-parser.d.ts +1 -1
- package/dist/codebase-index/rs-parser.d.ts.map +1 -1
- package/dist/codebase-index/schema.d.ts +9 -2
- package/dist/codebase-index/schema.d.ts.map +1 -1
- package/dist/codebase-index/spawn-gate.d.ts +15 -0
- package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
- package/dist/codebase-index/ts-parser.d.ts +2 -2
- package/dist/codebase-index/ts-parser.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +993 -348
- package/dist/codebase-index/worker.js.map +4 -4
- package/dist/codebase-index/writer.d.ts +83 -4
- package/dist/codebase-index/writer.d.ts.map +1 -1
- package/dist/codebase-index/yaml-parser.d.ts +1 -1
- package/dist/design.d.ts +5 -2
- package/dist/design.d.ts.map +1 -1
- package/dist/design.js +36 -3
- package/dist/design.js.map +2 -2
- package/dist/diff.d.ts +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +2 -2
- package/dist/diff.js.map +1 -1
- package/dist/document.d.ts +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +1 -1
- package/dist/document.js.map +1 -1
- package/dist/e2e.d.ts +1 -1
- package/dist/e2e.d.ts.map +1 -1
- package/dist/e2e.js +1 -1
- package/dist/e2e.js.map +1 -1
- package/dist/edit.d.ts +1 -1
- package/dist/edit.d.ts.map +1 -1
- package/dist/edit.js +3 -3
- package/dist/edit.js.map +2 -2
- package/dist/exec.d.ts +1 -1
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +99 -44
- package/dist/exec.js.map +3 -3
- package/dist/fetch.d.ts +1 -1
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +4 -5
- package/dist/fetch.js.map +2 -2
- package/dist/format.d.ts +1 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +81 -16
- package/dist/format.js.map +2 -2
- package/dist/git.d.ts +1 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +3 -2
- package/dist/git.js.map +2 -2
- package/dist/glob.d.ts +1 -1
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +81 -31
- package/dist/glob.js.map +4 -4
- package/dist/grep.d.ts +1 -1
- package/dist/grep.d.ts.map +1 -1
- package/dist/grep.js +90 -22
- package/dist/grep.js.map +4 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1895 -771
- package/dist/index.js.map +4 -4
- package/dist/install.d.ts +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +85 -17
- package/dist/install.js.map +2 -2
- package/dist/json.d.ts +1 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -2
- package/dist/json.js.map +2 -2
- package/dist/kanban-evidence-bridge.d.ts +21 -0
- package/dist/kanban-evidence-bridge.d.ts.map +1 -0
- package/dist/kanban.d.ts +20 -3
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +256 -32
- package/dist/kanban.js.map +3 -3
- package/dist/languages/execute-tool.d.ts +1 -1
- package/dist/languages/execute-tool.d.ts.map +1 -1
- package/dist/languages/execute.d.ts +1 -1
- package/dist/languages/execute.d.ts.map +1 -1
- package/dist/languages/index.d.ts +1 -1
- package/dist/languages/index.d.ts.map +1 -1
- package/dist/languages/index.js +99 -17
- package/dist/languages/index.js.map +2 -2
- package/dist/languages/package-tool.d.ts +3 -4
- package/dist/languages/package-tool.d.ts.map +1 -1
- package/dist/languages/plan.d.ts.map +1 -1
- package/dist/languages/profiles/additional.d.ts.map +1 -1
- package/dist/languages/profiles/primary.d.ts.map +1 -1
- package/dist/languages/tool.d.ts +1 -1
- package/dist/languages/tool.d.ts.map +1 -1
- package/dist/lint.d.ts +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +81 -16
- package/dist/lint.js.map +2 -2
- package/dist/logs.d.ts +1 -1
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +2 -2
- package/dist/logs.js.map +1 -1
- package/dist/memory.d.ts +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +1 -1
- package/dist/memory.js.map +1 -1
- package/dist/mode.d.ts +1 -1
- package/dist/mode.d.ts.map +1 -1
- package/dist/mode.js.map +1 -1
- package/dist/outdated.d.ts +1 -1
- package/dist/outdated.d.ts.map +1 -1
- package/dist/outdated.js +101 -19
- package/dist/outdated.js.map +2 -2
- package/dist/pack.d.ts +1 -1
- package/dist/pack.js +1650 -640
- package/dist/pack.js.map +4 -4
- package/dist/patch.d.ts +1 -1
- package/dist/patch.d.ts.map +1 -1
- package/dist/patch.js +2 -2
- package/dist/patch.js.map +1 -1
- package/dist/plan.d.ts +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +7 -7
- package/dist/plan.js.map +2 -2
- package/dist/process-guardian.d.ts +7 -0
- package/dist/process-guardian.d.ts.map +1 -1
- package/dist/process-registry-persistent.d.ts +5 -0
- package/dist/process-registry-persistent.d.ts.map +1 -1
- package/dist/process-registry.d.ts +9 -1
- package/dist/process-registry.d.ts.map +1 -1
- package/dist/process-registry.js +18 -3
- package/dist/process-registry.js.map +2 -2
- package/dist/ps-slash.js +93 -38
- package/dist/ps-slash.js.map +2 -2
- package/dist/read.d.ts +1 -1
- package/dist/read.d.ts.map +1 -1
- package/dist/read.js +3 -2
- package/dist/read.js.map +2 -2
- package/dist/replace.d.ts +1 -1
- package/dist/replace.d.ts.map +1 -1
- package/dist/replace.js +4 -4
- package/dist/replace.js.map +2 -2
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +2 -2
- package/dist/scaffold.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +4 -4
- package/dist/search.js.map +2 -2
- package/dist/session-kanban.d.ts +4 -1
- package/dist/session-kanban.d.ts.map +1 -1
- package/dist/session-kanban.js +23 -15
- package/dist/session-kanban.js.map +2 -2
- package/dist/set-working-dir.d.ts +1 -1
- package/dist/set-working-dir.d.ts.map +1 -1
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +2 -5
- package/dist/skill.js.map +2 -2
- package/dist/spawn-background.d.ts.map +1 -1
- package/dist/task.d.ts +3 -2
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +6 -8
- package/dist/task.js.map +2 -2
- package/dist/test.d.ts +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +81 -16
- package/dist/test.js.map +2 -2
- package/dist/todo.d.ts +2 -1
- package/dist/todo.d.ts.map +1 -1
- package/dist/todo.js +4 -4
- package/dist/todo.js.map +2 -2
- package/dist/tool-help.d.ts +1 -1
- package/dist/tool-help.d.ts.map +1 -1
- package/dist/tool-help.js.map +1 -1
- package/dist/tool-search.d.ts +1 -1
- package/dist/tool-search.d.ts.map +1 -1
- package/dist/tool-search.js.map +1 -1
- package/dist/tool-tier.d.ts +16 -0
- package/dist/tool-tier.d.ts.map +1 -0
- package/dist/tool-tier.js +22402 -0
- package/dist/tool-tier.js.map +7 -0
- package/dist/tool-use.d.ts +1 -1
- package/dist/tool-use.d.ts.map +1 -1
- package/dist/tool-use.js.map +1 -1
- package/dist/tree.d.ts +1 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +3 -15
- package/dist/tree.js.map +2 -2
- package/dist/typecheck.d.ts +1 -1
- package/dist/typecheck.d.ts.map +1 -1
- package/dist/typecheck.js +81 -16
- package/dist/typecheck.js.map +2 -2
- package/dist/win32.d.ts +10 -0
- package/dist/win32.d.ts.map +1 -0
- package/dist/win32.js +67 -0
- package/dist/win32.js.map +7 -0
- package/dist/write.d.ts +1 -1
- package/dist/write.d.ts.map +1 -1
- package/dist/write.js +3 -2
- package/dist/write.js.map +2 -2
- package/package.json +11 -3
|
@@ -86,6 +86,27 @@ export interface RepetitionSignal {
|
|
|
86
86
|
*/
|
|
87
87
|
shouldHalt: boolean;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Steer text appended (once) to a grounded todo continuation when the board
|
|
91
|
+
* has not moved between two consecutive automatic turns. Grounded prompts are
|
|
92
|
+
* synthesized from durable todo state, so an identical re-feed means the whole
|
|
93
|
+
* previous turn produced zero board movement — usually the model worked but
|
|
94
|
+
* forgot to update the board. One explicit steer fixes that far more often
|
|
95
|
+
* than halting; the halt only fires if the board stays frozen AFTER the steer.
|
|
96
|
+
*/
|
|
97
|
+
export declare const GROUNDED_NO_PROGRESS_STEER: string;
|
|
98
|
+
export type GroundedRepetitionAction = 'feed' | 'steer' | 'halt';
|
|
99
|
+
export interface GroundedRepetitionSignal extends RepetitionSignal {
|
|
100
|
+
/**
|
|
101
|
+
* What the caller should do with this grounded (todo-sourced) prompt:
|
|
102
|
+
* - `feed` — no repetition; feed the prompt as-is.
|
|
103
|
+
* - `steer` — first repetition; feed the prompt with
|
|
104
|
+
* {@link GROUNDED_NO_PROGRESS_STEER} appended instead of halting.
|
|
105
|
+
* - `halt` — the board stayed frozen even after a steer; break the loop
|
|
106
|
+
* exactly like a `shouldHalt` from {@link AutoProceedLoopGuard.record}.
|
|
107
|
+
*/
|
|
108
|
+
action: GroundedRepetitionAction;
|
|
109
|
+
}
|
|
89
110
|
export interface AutoProceedLoopGuard {
|
|
90
111
|
/**
|
|
91
112
|
* Record a prompt that is about to be auto-fed. Returns the repetition
|
|
@@ -94,6 +115,16 @@ export interface AutoProceedLoopGuard {
|
|
|
94
115
|
* the loop must be broken instead.
|
|
95
116
|
*/
|
|
96
117
|
record(prompt: string): RepetitionSignal;
|
|
118
|
+
/**
|
|
119
|
+
* Record a GROUNDED prompt — one synthesized from durable state (the todo
|
|
120
|
+
* board) rather than echoed from model output. Repetition of a grounded
|
|
121
|
+
* prompt is a "no board progress" signal, not necessarily an echo loop, so
|
|
122
|
+
* the first repetition asks the caller to steer (append
|
|
123
|
+
* {@link GROUNDED_NO_PROGRESS_STEER} to the fed text) and only a repetition
|
|
124
|
+
* that survives the steer halts. The steer state resets whenever a
|
|
125
|
+
* different prompt is recorded (board moved) or on {@link reset}.
|
|
126
|
+
*/
|
|
127
|
+
recordGrounded(prompt: string): GroundedRepetitionSignal;
|
|
97
128
|
/**
|
|
98
129
|
* Drop the history. Call this on any manual user input so a fresh run
|
|
99
130
|
* starts with no memory of the prior cycle.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-proceed-loop-guard.d.ts","sourceRoot":"","sources":["../src/auto-proceed-loop-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACzC;;;OAGG;IACH,KAAK,IAAI,IAAI,CAAC;IACd;;;;OAIG;IACH,OAAO,IAAI,SAAS,MAAM,EAAE,CAAC;IAC7B;;OAEG;IACH,IAAI,IAAI,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,gBAAqB,GAAG,oBAAoB,
|
|
1
|
+
{"version":3,"file":"auto-proceed-loop-guard.d.ts","sourceRoot":"","sources":["../src/auto-proceed-loop-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,QAO3B,CAAC;AAEb,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,wBAAyB,SAAQ,gBAAgB;IAChE;;;;;;;OAOG;IACH,MAAM,EAAE,wBAAwB,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACzC;;;;;;;;OAQG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,wBAAwB,CAAC;IACzD;;;OAGG;IACH,KAAK,IAAI,IAAI,CAAC;IACd;;;;OAIG;IACH,OAAO,IAAI,SAAS,MAAM,EAAE,CAAC;IAC7B;;OAEG;IACH,IAAI,IAAI,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,gBAAqB,GAAG,oBAAoB,CAqE/F"}
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
function normalizeForRepetition(prompt) {
|
|
3
3
|
return prompt.replace(/\s+/g, " ").trim().toLowerCase();
|
|
4
4
|
}
|
|
5
|
+
var GROUNDED_NO_PROGRESS_STEER = [
|
|
6
|
+
"NOTE: The todo board has not changed since the previous automatic continuation \u2014 this exact instruction is being repeated.",
|
|
7
|
+
"Before anything else, bring the board up to date so the loop stays grounded:",
|
|
8
|
+
"- mark work that is actually finished as completed,",
|
|
9
|
+
"- split the current item into smaller concrete sub-items if it needs more than one turn,",
|
|
10
|
+
"- remove or reword items that no longer apply.",
|
|
11
|
+
"Then continue the work. If you are blocked, state exactly what is blocking you instead of repeating the same turn."
|
|
12
|
+
].join("\n");
|
|
5
13
|
function createAutoProceedLoopGuard(options = {}) {
|
|
6
14
|
const safeIntegerOption = (value, fallback) => {
|
|
7
15
|
if (value === void 0) return fallback;
|
|
@@ -11,6 +19,7 @@ function createAutoProceedLoopGuard(options = {}) {
|
|
|
11
19
|
const repeatThreshold = Math.max(2, safeIntegerOption(options.repeatThreshold, 2));
|
|
12
20
|
const windowSize = Math.max(repeatThreshold, safeIntegerOption(options.windowSize, 3));
|
|
13
21
|
let buffer = [];
|
|
22
|
+
let steeredFor = null;
|
|
14
23
|
function record(prompt) {
|
|
15
24
|
const normalized = normalizeForRepetition(prompt);
|
|
16
25
|
buffer.push(normalized);
|
|
@@ -28,8 +37,21 @@ function createAutoProceedLoopGuard(options = {}) {
|
|
|
28
37
|
shouldHalt: runLength >= repeatThreshold
|
|
29
38
|
};
|
|
30
39
|
}
|
|
40
|
+
function recordGrounded(prompt) {
|
|
41
|
+
const signal = record(prompt);
|
|
42
|
+
if (!signal.shouldHalt) {
|
|
43
|
+
steeredFor = null;
|
|
44
|
+
return { ...signal, shouldHalt: false, action: "feed" };
|
|
45
|
+
}
|
|
46
|
+
if (steeredFor === signal.normalized) {
|
|
47
|
+
return { ...signal, action: "halt" };
|
|
48
|
+
}
|
|
49
|
+
steeredFor = signal.normalized;
|
|
50
|
+
return { ...signal, shouldHalt: false, action: "steer" };
|
|
51
|
+
}
|
|
31
52
|
function reset() {
|
|
32
53
|
buffer = [];
|
|
54
|
+
steeredFor = null;
|
|
33
55
|
}
|
|
34
56
|
function history() {
|
|
35
57
|
return buffer.slice();
|
|
@@ -37,9 +59,10 @@ function createAutoProceedLoopGuard(options = {}) {
|
|
|
37
59
|
function size() {
|
|
38
60
|
return buffer.length;
|
|
39
61
|
}
|
|
40
|
-
return { record, reset, history, size };
|
|
62
|
+
return { record, recordGrounded, reset, history, size };
|
|
41
63
|
}
|
|
42
64
|
export {
|
|
65
|
+
GROUNDED_NO_PROGRESS_STEER,
|
|
43
66
|
createAutoProceedLoopGuard,
|
|
44
67
|
normalizeForRepetition
|
|
45
68
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/auto-proceed-loop-guard.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Auto-proceed loop guard.\n *\n * ## Problem\n *\n * The REPL and TUI both implement an \"auto\" autonomy mode that, after every\n * completed agent turn, feeds the next suggestion (or top auto=\"true\" item)\n * back as a fresh prompt. When the agent emits the same `<nextsteps>` block\n * on every reply \u2014 which happens whenever the model's output is stable but\n * autonomy treats the next step as \"still actionable\" \u2014 the loop self-feeds\n * the same instruction 2\u20133 times in a row before the longer\n * `autoProceedMaxIterations` cap (default 50) trips.\n *\n * The user reports the visible symptom: the same response, including the same\n * `<nextsteps>` block, repeats. Manual input from the user is the only thing\n * that breaks out. They suspect the next-steps mechanism is the source.\n *\n * The 50-iteration cap is intentionally loose \u2014 it is a runaway safety net,\n * not a UX signal. By the time it trips, the user has already watched the\n * same response 50 times.\n *\n * ## Approach\n *\n * A small, pure, browser-safe stateful helper that records the last few\n * prompts fed through the auto-proceed / auto-submit path. It normalizes\n * prompts (whitespace, casing) so trivial rewordings are not collapsed, but\n * identical re-feeds are. When the same normalized prompt is fed\n * `repeatThreshold` (default 2) times in a row, the guard returns\n * `{ shouldHalt: true }`.\n *\n * Callers are expected to:\n * 1. Call `record(prompt)` immediately before each auto-feed.\n * 2. If `shouldHalt` is true, clear the suggestion + auto-suggestion store,\n * cancel any pending countdown, and surface a \"we detected a loop\"\n * message asking the user what's happening. Do NOT feed the prompt.\n * 3. Call `reset()` whenever the user types anything manually (REPL manual\n * input or any user-driven submit) so the next auto-feed starts clean.\n *\n * The default `repeatThreshold` is 2 \u2014 i.e. the second consecutive identical\n * feed (the first re-feed of an already-seen prompt) halts the loop. That\n * matches the user's report (\"receiving the same prompt 2-3 times in a row\n * causes the system to enter a loop\"). Setting it to 2 catches the loop on\n * the first identical re-feed rather than after the 50-iteration runaway cap trips.\n *\n * The window size is intentionally small (default 3). The loop we care about\n * is the immediate repetition \u2014 a longer history would misfire on legitimate\n * \"do X, then do Y, then do X\" sequences the model occasionally returns to.\n *\n * This module is BROWSER-SAFE \u2014 no Node-only imports \u2014 so it can be imported\n * from Vite-bundled WebUI as well as Node-based CLI/TUI.\n */\n\n/**\n * Normalize a prompt for repetition comparison. Whitespace is collapsed and\n * trimmed; case is folded to lowercase. Nothing semantic is removed, so two\n * prompts that differ in any meaningful way (extra word, different code,\n * different file path) compare as distinct. Two prompts that differ only by\n * leading/trailing whitespace and casing are treated as identical.\n */\nexport function normalizeForRepetition(prompt: string): string {\n return prompt.replace(/\\s+/g, ' ').trim().toLowerCase();\n}\n\nexport interface LoopGuardOptions {\n /**\n * How many consecutive identical prompts (after normalization) trigger a\n * halt. Default 2 \u2014 i.e. the second consecutive identical feed (the first\n * re-feed of an already-seen prompt) halts the loop. Clamped to >= 2.\n */\n repeatThreshold?: number;\n /**\n * How many of the most recent feeds to retain for comparison. The window\n * is searched from newest to oldest; the guard cares about the run of\n * identical entries ending at the most recent one, not about identical\n * entries anywhere in history. Default 3.\n */\n windowSize?: number;\n}\n\nexport interface RepetitionSignal {\n /** The normalized prompt that just got recorded. */\n normalized: string;\n /**\n * Number of consecutive identical feeds ending at the most recent feed,\n * including this one. Always >= 1.\n */\n runLength: number;\n /**\n * True when the run length has crossed `repeatThreshold`. Callers must\n * stop feeding and surface a user prompt instead.\n */\n shouldHalt: boolean;\n}\n\nexport interface AutoProceedLoopGuard {\n /**\n * Record a prompt that is about to be auto-fed. Returns the repetition\n * signal for that prompt. When `shouldHalt` is true the caller MUST NOT\n * feed the prompt \u2014 it has been recorded for post-mortem inspection, but\n * the loop must be broken instead.\n */\n record(prompt: string): RepetitionSignal;\n /**\n * Drop the history. Call this on any manual user input so a fresh run\n * starts with no memory of the prior cycle.\n */\n reset(): void;\n /**\n * Read-only view of the most recent normalized prompts (newest last).\n * Useful for diagnostics and for the message shown to the user when the\n * guard halts the loop.\n */\n history(): readonly string[];\n /**\n * How many entries the guard is currently retaining.\n */\n size(): number;\n}\n\n/**\n * Build a fresh loop guard. Defaults: `repeatThreshold = 2`, `windowSize = 3`.\n *\n * @example\n * const guard = createAutoProceedLoopGuard();\n * for (const prompt of candidates) {\n * const signal = guard.record(prompt);\n * if (signal.shouldHalt) {\n * haltAutoProceedAndAskUser(signal);\n * break;\n * }\n * feedPrompt(prompt);\n * }\n */\nexport function createAutoProceedLoopGuard(options: LoopGuardOptions = {}): AutoProceedLoopGuard {\n // Validate options defensively. Non-finite values (NaN, Infinity) MUST\n // fall back to the documented default \u2014 a guard that silently disables\n // itself is worse than no guard at all. Specifically:\n // - `repeatThreshold: NaN` would cause `Math.floor(NaN) = NaN`,\n // `Math.max(2, NaN) = NaN`, and `runLength >= NaN` is always false,\n // making the guard permanent no-op.\n // - `windowSize: NaN` / `Infinity` would skip the trim branch entirely\n // (`buffer.length > NaN` and `buffer.length > Infinity` are both\n // false), so the buffer would grow without bound.\n const safeIntegerOption = (value: number | undefined, fallback: number): number => {\n if (value === undefined) return fallback;\n if (!Number.isFinite(value)) return fallback;\n return Math.floor(value);\n };\n const repeatThreshold = Math.max(2, safeIntegerOption(options.repeatThreshold, 2));\n const windowSize = Math.max(repeatThreshold, safeIntegerOption(options.windowSize, 3));\n let buffer: string[] = [];\n\n function record(prompt: string): RepetitionSignal {\n const normalized = normalizeForRepetition(prompt);\n buffer.push(normalized);\n if (buffer.length > windowSize) {\n buffer = buffer.slice(buffer.length - windowSize);\n }\n // Count the trailing run of identical normalized prompts.\n let runLength = 0;\n for (let i = buffer.length - 1; i >= 0; i--) {\n if (buffer[i] === normalized) runLength++;\n else break;\n }\n return {\n normalized,\n runLength,\n shouldHalt: runLength >= repeatThreshold,\n };\n }\n\n function reset(): void {\n buffer = [];\n }\n\n function history(): readonly string[] {\n return buffer.slice();\n }\n\n function size(): number {\n return buffer.length;\n }\n\n return { record, reset, history, size };\n}\n"],
|
|
5
|
-
"mappings": ";AA2DO,SAAS,uBAAuB,QAAwB;AAC7D,SAAO,OAAO,QAAQ,QAAQ,GAAG,EAAE,KAAK,EAAE,YAAY;AACxD;
|
|
4
|
+
"sourcesContent": ["/**\n * Auto-proceed loop guard.\n *\n * ## Problem\n *\n * The REPL and TUI both implement an \"auto\" autonomy mode that, after every\n * completed agent turn, feeds the next suggestion (or top auto=\"true\" item)\n * back as a fresh prompt. When the agent emits the same `<nextsteps>` block\n * on every reply \u2014 which happens whenever the model's output is stable but\n * autonomy treats the next step as \"still actionable\" \u2014 the loop self-feeds\n * the same instruction 2\u20133 times in a row before the longer\n * `autoProceedMaxIterations` cap (default 50) trips.\n *\n * The user reports the visible symptom: the same response, including the same\n * `<nextsteps>` block, repeats. Manual input from the user is the only thing\n * that breaks out. They suspect the next-steps mechanism is the source.\n *\n * The 50-iteration cap is intentionally loose \u2014 it is a runaway safety net,\n * not a UX signal. By the time it trips, the user has already watched the\n * same response 50 times.\n *\n * ## Approach\n *\n * A small, pure, browser-safe stateful helper that records the last few\n * prompts fed through the auto-proceed / auto-submit path. It normalizes\n * prompts (whitespace, casing) so trivial rewordings are not collapsed, but\n * identical re-feeds are. When the same normalized prompt is fed\n * `repeatThreshold` (default 2) times in a row, the guard returns\n * `{ shouldHalt: true }`.\n *\n * Callers are expected to:\n * 1. Call `record(prompt)` immediately before each auto-feed.\n * 2. If `shouldHalt` is true, clear the suggestion + auto-suggestion store,\n * cancel any pending countdown, and surface a \"we detected a loop\"\n * message asking the user what's happening. Do NOT feed the prompt.\n * 3. Call `reset()` whenever the user types anything manually (REPL manual\n * input or any user-driven submit) so the next auto-feed starts clean.\n *\n * The default `repeatThreshold` is 2 \u2014 i.e. the second consecutive identical\n * feed (the first re-feed of an already-seen prompt) halts the loop. That\n * matches the user's report (\"receiving the same prompt 2-3 times in a row\n * causes the system to enter a loop\"). Setting it to 2 catches the loop on\n * the first identical re-feed rather than after the 50-iteration runaway cap trips.\n *\n * The window size is intentionally small (default 3). The loop we care about\n * is the immediate repetition \u2014 a longer history would misfire on legitimate\n * \"do X, then do Y, then do X\" sequences the model occasionally returns to.\n *\n * This module is BROWSER-SAFE \u2014 no Node-only imports \u2014 so it can be imported\n * from Vite-bundled WebUI as well as Node-based CLI/TUI.\n */\n\n/**\n * Normalize a prompt for repetition comparison. Whitespace is collapsed and\n * trimmed; case is folded to lowercase. Nothing semantic is removed, so two\n * prompts that differ in any meaningful way (extra word, different code,\n * different file path) compare as distinct. Two prompts that differ only by\n * leading/trailing whitespace and casing are treated as identical.\n */\nexport function normalizeForRepetition(prompt: string): string {\n return prompt.replace(/\\s+/g, ' ').trim().toLowerCase();\n}\n\nexport interface LoopGuardOptions {\n /**\n * How many consecutive identical prompts (after normalization) trigger a\n * halt. Default 2 \u2014 i.e. the second consecutive identical feed (the first\n * re-feed of an already-seen prompt) halts the loop. Clamped to >= 2.\n */\n repeatThreshold?: number;\n /**\n * How many of the most recent feeds to retain for comparison. The window\n * is searched from newest to oldest; the guard cares about the run of\n * identical entries ending at the most recent one, not about identical\n * entries anywhere in history. Default 3.\n */\n windowSize?: number;\n}\n\nexport interface RepetitionSignal {\n /** The normalized prompt that just got recorded. */\n normalized: string;\n /**\n * Number of consecutive identical feeds ending at the most recent feed,\n * including this one. Always >= 1.\n */\n runLength: number;\n /**\n * True when the run length has crossed `repeatThreshold`. Callers must\n * stop feeding and surface a user prompt instead.\n */\n shouldHalt: boolean;\n}\n\n/**\n * Steer text appended (once) to a grounded todo continuation when the board\n * has not moved between two consecutive automatic turns. Grounded prompts are\n * synthesized from durable todo state, so an identical re-feed means the whole\n * previous turn produced zero board movement \u2014 usually the model worked but\n * forgot to update the board. One explicit steer fixes that far more often\n * than halting; the halt only fires if the board stays frozen AFTER the steer.\n */\nexport const GROUNDED_NO_PROGRESS_STEER = [\n 'NOTE: The todo board has not changed since the previous automatic continuation \u2014 this exact instruction is being repeated.',\n 'Before anything else, bring the board up to date so the loop stays grounded:',\n '- mark work that is actually finished as completed,',\n '- split the current item into smaller concrete sub-items if it needs more than one turn,',\n '- remove or reword items that no longer apply.',\n 'Then continue the work. If you are blocked, state exactly what is blocking you instead of repeating the same turn.',\n].join('\\n');\n\nexport type GroundedRepetitionAction = 'feed' | 'steer' | 'halt';\n\nexport interface GroundedRepetitionSignal extends RepetitionSignal {\n /**\n * What the caller should do with this grounded (todo-sourced) prompt:\n * - `feed` \u2014 no repetition; feed the prompt as-is.\n * - `steer` \u2014 first repetition; feed the prompt with\n * {@link GROUNDED_NO_PROGRESS_STEER} appended instead of halting.\n * - `halt` \u2014 the board stayed frozen even after a steer; break the loop\n * exactly like a `shouldHalt` from {@link AutoProceedLoopGuard.record}.\n */\n action: GroundedRepetitionAction;\n}\n\nexport interface AutoProceedLoopGuard {\n /**\n * Record a prompt that is about to be auto-fed. Returns the repetition\n * signal for that prompt. When `shouldHalt` is true the caller MUST NOT\n * feed the prompt \u2014 it has been recorded for post-mortem inspection, but\n * the loop must be broken instead.\n */\n record(prompt: string): RepetitionSignal;\n /**\n * Record a GROUNDED prompt \u2014 one synthesized from durable state (the todo\n * board) rather than echoed from model output. Repetition of a grounded\n * prompt is a \"no board progress\" signal, not necessarily an echo loop, so\n * the first repetition asks the caller to steer (append\n * {@link GROUNDED_NO_PROGRESS_STEER} to the fed text) and only a repetition\n * that survives the steer halts. The steer state resets whenever a\n * different prompt is recorded (board moved) or on {@link reset}.\n */\n recordGrounded(prompt: string): GroundedRepetitionSignal;\n /**\n * Drop the history. Call this on any manual user input so a fresh run\n * starts with no memory of the prior cycle.\n */\n reset(): void;\n /**\n * Read-only view of the most recent normalized prompts (newest last).\n * Useful for diagnostics and for the message shown to the user when the\n * guard halts the loop.\n */\n history(): readonly string[];\n /**\n * How many entries the guard is currently retaining.\n */\n size(): number;\n}\n\n/**\n * Build a fresh loop guard. Defaults: `repeatThreshold = 2`, `windowSize = 3`.\n *\n * @example\n * const guard = createAutoProceedLoopGuard();\n * for (const prompt of candidates) {\n * const signal = guard.record(prompt);\n * if (signal.shouldHalt) {\n * haltAutoProceedAndAskUser(signal);\n * break;\n * }\n * feedPrompt(prompt);\n * }\n */\nexport function createAutoProceedLoopGuard(options: LoopGuardOptions = {}): AutoProceedLoopGuard {\n // Validate options defensively. Non-finite values (NaN, Infinity) MUST\n // fall back to the documented default \u2014 a guard that silently disables\n // itself is worse than no guard at all. Specifically:\n // - `repeatThreshold: NaN` would cause `Math.floor(NaN) = NaN`,\n // `Math.max(2, NaN) = NaN`, and `runLength >= NaN` is always false,\n // making the guard permanent no-op.\n // - `windowSize: NaN` / `Infinity` would skip the trim branch entirely\n // (`buffer.length > NaN` and `buffer.length > Infinity` are both\n // false), so the buffer would grow without bound.\n const safeIntegerOption = (value: number | undefined, fallback: number): number => {\n if (value === undefined) return fallback;\n if (!Number.isFinite(value)) return fallback;\n return Math.floor(value);\n };\n const repeatThreshold = Math.max(2, safeIntegerOption(options.repeatThreshold, 2));\n const windowSize = Math.max(repeatThreshold, safeIntegerOption(options.windowSize, 3));\n let buffer: string[] = [];\n // Normalized grounded prompt we already steered for; a halt only fires for\n // a grounded prompt that repeats again AFTER its steer.\n let steeredFor: string | null = null;\n\n function record(prompt: string): RepetitionSignal {\n const normalized = normalizeForRepetition(prompt);\n buffer.push(normalized);\n if (buffer.length > windowSize) {\n buffer = buffer.slice(buffer.length - windowSize);\n }\n // Count the trailing run of identical normalized prompts.\n let runLength = 0;\n for (let i = buffer.length - 1; i >= 0; i--) {\n if (buffer[i] === normalized) runLength++;\n else break;\n }\n return {\n normalized,\n runLength,\n shouldHalt: runLength >= repeatThreshold,\n };\n }\n\n function recordGrounded(prompt: string): GroundedRepetitionSignal {\n const signal = record(prompt);\n if (!signal.shouldHalt) {\n // The board moved (different prompt) \u2014 clear any pending steer state.\n steeredFor = null;\n return { ...signal, shouldHalt: false, action: 'feed' };\n }\n if (steeredFor === signal.normalized) {\n return { ...signal, action: 'halt' };\n }\n steeredFor = signal.normalized;\n return { ...signal, shouldHalt: false, action: 'steer' };\n }\n\n function reset(): void {\n buffer = [];\n steeredFor = null;\n }\n\n function history(): readonly string[] {\n return buffer.slice();\n }\n\n function size(): number {\n return buffer.length;\n }\n\n return { record, recordGrounded, reset, history, size };\n}\n"],
|
|
5
|
+
"mappings": ";AA2DO,SAAS,uBAAuB,QAAwB;AAC7D,SAAO,OAAO,QAAQ,QAAQ,GAAG,EAAE,KAAK,EAAE,YAAY;AACxD;AAyCO,IAAM,6BAA6B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,IAAI;AAiEJ,SAAS,2BAA2B,UAA4B,CAAC,GAAyB;AAU/F,QAAM,oBAAoB,CAAC,OAA2B,aAA6B;AACjF,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,WAAO,KAAK,MAAM,KAAK;AAAA,EACzB;AACA,QAAM,kBAAkB,KAAK,IAAI,GAAG,kBAAkB,QAAQ,iBAAiB,CAAC,CAAC;AACjF,QAAM,aAAa,KAAK,IAAI,iBAAiB,kBAAkB,QAAQ,YAAY,CAAC,CAAC;AACrF,MAAI,SAAmB,CAAC;AAGxB,MAAI,aAA4B;AAEhC,WAAS,OAAO,QAAkC;AAChD,UAAM,aAAa,uBAAuB,MAAM;AAChD,WAAO,KAAK,UAAU;AACtB,QAAI,OAAO,SAAS,YAAY;AAC9B,eAAS,OAAO,MAAM,OAAO,SAAS,UAAU;AAAA,IAClD;AAEA,QAAI,YAAY;AAChB,aAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,UAAI,OAAO,CAAC,MAAM,WAAY;AAAA,UACzB;AAAA,IACP;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,YAAY,aAAa;AAAA,IAC3B;AAAA,EACF;AAEA,WAAS,eAAe,QAA0C;AAChE,UAAM,SAAS,OAAO,MAAM;AAC5B,QAAI,CAAC,OAAO,YAAY;AAEtB,mBAAa;AACb,aAAO,EAAE,GAAG,QAAQ,YAAY,OAAO,QAAQ,OAAO;AAAA,IACxD;AACA,QAAI,eAAe,OAAO,YAAY;AACpC,aAAO,EAAE,GAAG,QAAQ,QAAQ,OAAO;AAAA,IACrC;AACA,iBAAa,OAAO;AACpB,WAAO,EAAE,GAAG,QAAQ,YAAY,OAAO,QAAQ,QAAQ;AAAA,EACzD;AAEA,WAAS,QAAc;AACrB,aAAS,CAAC;AACV,iBAAa;AAAA,EACf;AAEA,WAAS,UAA6B;AACpC,WAAO,OAAO,MAAM;AAAA,EACtB;AAEA,WAAS,OAAe;AACtB,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO,EAAE,QAAQ,gBAAgB,OAAO,SAAS,KAAK;AACxD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/bash.d.ts
CHANGED
package/dist/bash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../src/bash.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../src/bash.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,IAAI,EAAmB,MAAM,wBAAwB,CAAC;AASpE,UAAU,SAAS;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC;AAED,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AA4BD,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAyoBhD,CAAC;AAGF,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC"}
|
package/dist/bash.js
CHANGED
|
@@ -5,16 +5,16 @@ import {
|
|
|
5
5
|
emitProcessCompleted,
|
|
6
6
|
emitProcessOutput,
|
|
7
7
|
emitProcessStarted
|
|
8
|
-
} from "@wrongstack/core";
|
|
8
|
+
} from "@wrongstack/core/observability";
|
|
9
9
|
|
|
10
10
|
// src/_env.ts
|
|
11
|
-
import { buildChildEnv } from "@wrongstack/core";
|
|
11
|
+
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
12
12
|
|
|
13
13
|
// src/_output-spool.ts
|
|
14
14
|
import { createWriteStream, mkdirSync } from "node:fs";
|
|
15
15
|
import * as fsp from "node:fs/promises";
|
|
16
16
|
import * as path from "node:path";
|
|
17
|
-
import { wstackGlobalRoot } from "@wrongstack/core";
|
|
17
|
+
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
18
18
|
var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
19
19
|
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
20
20
|
var sweepStarted = false;
|
|
@@ -118,7 +118,7 @@ function createOutputSpool(opts) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// src/_util.ts
|
|
121
|
-
import * as Core from "@wrongstack/core";
|
|
121
|
+
import * as Core from "@wrongstack/core/utils";
|
|
122
122
|
var COMMAND_OUTPUT_MAX_BYTES = 32768;
|
|
123
123
|
var REPEAT_RUN_THRESHOLD = 3;
|
|
124
124
|
function collapseCarriageReturns(text) {
|
|
@@ -387,7 +387,7 @@ var CircuitBreaker = class {
|
|
|
387
387
|
};
|
|
388
388
|
|
|
389
389
|
// src/_redact-command.ts
|
|
390
|
-
import { expectDefined } from "@wrongstack/core";
|
|
390
|
+
import { expectDefined } from "@wrongstack/core/utils";
|
|
391
391
|
var SENSITIVE_FLAG_PATTERNS = [
|
|
392
392
|
// --flag=value or --flag "value" (value captured up to next space or comma)
|
|
393
393
|
/--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
|
|
@@ -481,7 +481,12 @@ var ProcessRegistryImpl = class {
|
|
|
481
481
|
this.breaker.setEnabled(false);
|
|
482
482
|
}
|
|
483
483
|
register(info) {
|
|
484
|
-
this.processes.set(info.pid, {
|
|
484
|
+
this.processes.set(info.pid, {
|
|
485
|
+
...info,
|
|
486
|
+
killed: false,
|
|
487
|
+
protected: info.protected ?? false,
|
|
488
|
+
background: info.background ?? false
|
|
489
|
+
});
|
|
485
490
|
}
|
|
486
491
|
_isSafeSignalPid(pid) {
|
|
487
492
|
return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
|
|
@@ -534,12 +539,21 @@ var ProcessRegistryImpl = class {
|
|
|
534
539
|
}
|
|
535
540
|
return n;
|
|
536
541
|
}
|
|
542
|
+
/** Count of active jobs explicitly launched in background mode. */
|
|
543
|
+
get activeBackgroundCount() {
|
|
544
|
+
let n = 0;
|
|
545
|
+
for (const p of this.processes.values()) {
|
|
546
|
+
if (p.background && !p.killed) n++;
|
|
547
|
+
}
|
|
548
|
+
return n;
|
|
549
|
+
}
|
|
537
550
|
/**
|
|
538
551
|
* Combined stats for observability — used by /ps and the TUI status bar.
|
|
539
552
|
*/
|
|
540
553
|
stats() {
|
|
541
554
|
return {
|
|
542
555
|
activeCount: this.activeCount,
|
|
556
|
+
backgroundCount: this.activeBackgroundCount,
|
|
543
557
|
totalCount: this.processes.size,
|
|
544
558
|
breaker: this.breaker.snapshot()
|
|
545
559
|
};
|
|
@@ -640,7 +654,7 @@ var ProcessRegistryImpl = class {
|
|
|
640
654
|
this.autoKillTimer = setTimeout(() => {
|
|
641
655
|
this.autoKillTimer = null;
|
|
642
656
|
this.autoKillArmedAt = null;
|
|
643
|
-
this.killAll({ force: false });
|
|
657
|
+
this.killAll({ force: false, preserveBackground: true });
|
|
644
658
|
this.breaker.forceReset();
|
|
645
659
|
this._emitBreakerCountdown();
|
|
646
660
|
}, this.autoKillResetMs);
|
|
@@ -680,6 +694,7 @@ var ProcessRegistryImpl = class {
|
|
|
680
694
|
if (!p) return false;
|
|
681
695
|
if (p.killed) return true;
|
|
682
696
|
if (p.protected) return false;
|
|
697
|
+
if (opts.preserveBackground && p.background) return false;
|
|
683
698
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
684
699
|
const isWin2 = os.platform() === "win32";
|
|
685
700
|
if (isWin2) {
|
|
@@ -790,8 +805,9 @@ import * as os3 from "node:os";
|
|
|
790
805
|
// src/process-registry-persistent.ts
|
|
791
806
|
import * as fs from "node:fs/promises";
|
|
792
807
|
import * as os2 from "node:os";
|
|
808
|
+
import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
|
|
793
809
|
import * as path2 from "node:path";
|
|
794
|
-
var REGISTRY_FILE = "
|
|
810
|
+
var REGISTRY_FILE = "process-registry.json";
|
|
795
811
|
function toErrorMessage(err) {
|
|
796
812
|
return err instanceof Error ? err.message : String(err);
|
|
797
813
|
}
|
|
@@ -809,7 +825,8 @@ function emitStructuredLog(level, event, message, error) {
|
|
|
809
825
|
}
|
|
810
826
|
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
811
827
|
var STALE_THRESHOLD_MS = 3e4;
|
|
812
|
-
var
|
|
828
|
+
var LOCK_STALE_MS = 3e4;
|
|
829
|
+
var LOCKFILE = ".process-registry.lock";
|
|
813
830
|
function generateInstanceId() {
|
|
814
831
|
const hostname2 = os2.hostname();
|
|
815
832
|
const pid = process.pid;
|
|
@@ -837,21 +854,26 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
|
|
|
837
854
|
try {
|
|
838
855
|
const content = await fs.readFile(lockfilePath, "utf-8");
|
|
839
856
|
const parts = content.split(":");
|
|
840
|
-
const
|
|
841
|
-
const
|
|
842
|
-
|
|
857
|
+
const lockPid = parseInt(parts[0] ?? "0", 10);
|
|
858
|
+
const lockTs = Number(parts[parts.length - 1]);
|
|
859
|
+
const staleByAge = Number.isFinite(lockTs) && Date.now() - lockTs > LOCK_STALE_MS;
|
|
860
|
+
let holderDead = false;
|
|
861
|
+
if (process.platform !== "win32" && Number.isFinite(lockPid) && lockPid > 0) {
|
|
843
862
|
try {
|
|
844
863
|
process.kill(lockPid, 0);
|
|
845
864
|
} catch {
|
|
846
|
-
|
|
847
|
-
continue;
|
|
865
|
+
holderDead = true;
|
|
848
866
|
}
|
|
849
867
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
868
|
+
if (holderDead || staleByAge) {
|
|
869
|
+
await fs.unlink(lockfilePath).catch(() => {
|
|
870
|
+
});
|
|
871
|
+
continue;
|
|
854
872
|
}
|
|
873
|
+
} catch {
|
|
874
|
+
await fs.unlink(lockfilePath).catch(() => {
|
|
875
|
+
});
|
|
876
|
+
continue;
|
|
855
877
|
}
|
|
856
878
|
await new Promise((r) => setTimeout(r, 100));
|
|
857
879
|
continue;
|
|
@@ -861,25 +883,35 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
|
|
|
861
883
|
}
|
|
862
884
|
throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
|
|
863
885
|
}
|
|
886
|
+
function freshRegistryData() {
|
|
887
|
+
return {
|
|
888
|
+
version: 1,
|
|
889
|
+
instances: /* @__PURE__ */ new Map(),
|
|
890
|
+
protectedPatterns: ["wrongstack", "node"],
|
|
891
|
+
lastCleanup: Date.now()
|
|
892
|
+
};
|
|
893
|
+
}
|
|
864
894
|
async function readRegistryFile(filePath) {
|
|
895
|
+
let content;
|
|
865
896
|
try {
|
|
866
|
-
|
|
867
|
-
const parsed = JSON.parse(content);
|
|
868
|
-
if (parsed.instances && Array.isArray(parsed.instances)) {
|
|
869
|
-
parsed.instances = new Map(parsed.instances);
|
|
870
|
-
}
|
|
871
|
-
return parsed;
|
|
897
|
+
content = await fs.readFile(filePath, "utf-8");
|
|
872
898
|
} catch (err) {
|
|
873
|
-
if (isNodeError(err) && err.code === "ENOENT")
|
|
874
|
-
return {
|
|
875
|
-
version: 1,
|
|
876
|
-
instances: /* @__PURE__ */ new Map(),
|
|
877
|
-
protectedPatterns: ["wrongstack", "node"],
|
|
878
|
-
lastCleanup: Date.now()
|
|
879
|
-
};
|
|
880
|
-
}
|
|
899
|
+
if (isNodeError(err) && err.code === "ENOENT") return freshRegistryData();
|
|
881
900
|
throw err;
|
|
882
901
|
}
|
|
902
|
+
try {
|
|
903
|
+
const parsed = JSON.parse(content);
|
|
904
|
+
if (!parsed || typeof parsed !== "object") return freshRegistryData();
|
|
905
|
+
const base = freshRegistryData();
|
|
906
|
+
return {
|
|
907
|
+
version: 1,
|
|
908
|
+
instances: Array.isArray(parsed.instances) ? new Map(parsed.instances) : base.instances,
|
|
909
|
+
protectedPatterns: Array.isArray(parsed.protectedPatterns) ? parsed.protectedPatterns : base.protectedPatterns,
|
|
910
|
+
lastCleanup: typeof parsed.lastCleanup === "number" ? parsed.lastCleanup : base.lastCleanup
|
|
911
|
+
};
|
|
912
|
+
} catch {
|
|
913
|
+
return freshRegistryData();
|
|
914
|
+
}
|
|
883
915
|
}
|
|
884
916
|
async function writeRegistryFile(filePath, data) {
|
|
885
917
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
@@ -898,12 +930,18 @@ var PersistentProcessRegistry = class {
|
|
|
898
930
|
lockPath;
|
|
899
931
|
baseRegistry;
|
|
900
932
|
heartbeatInterval = null;
|
|
933
|
+
cleanupInterval = null;
|
|
934
|
+
heartbeatRunning = false;
|
|
935
|
+
cleanupRunning = false;
|
|
901
936
|
isShuttingDown = false;
|
|
937
|
+
onProcessExit = () => {
|
|
938
|
+
void this.syncToPersistent();
|
|
939
|
+
};
|
|
902
940
|
constructor(baseRegistry) {
|
|
903
941
|
this.instanceId = generateInstanceId();
|
|
904
|
-
const
|
|
905
|
-
this.registryPath = path2.join(
|
|
906
|
-
this.lockPath = path2.join(
|
|
942
|
+
const globalRoot = wstackGlobalRoot3();
|
|
943
|
+
this.registryPath = path2.join(globalRoot, REGISTRY_FILE);
|
|
944
|
+
this.lockPath = path2.join(globalRoot, LOCKFILE);
|
|
907
945
|
this.baseRegistry = baseRegistry ?? getProcessRegistry();
|
|
908
946
|
this.ensureDirectory().catch((err) => {
|
|
909
947
|
emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
|
|
@@ -922,16 +960,18 @@ var PersistentProcessRegistry = class {
|
|
|
922
960
|
*/
|
|
923
961
|
start() {
|
|
924
962
|
if (this.heartbeatInterval) return;
|
|
925
|
-
this.
|
|
963
|
+
this.isShuttingDown = false;
|
|
964
|
+
this.heartbeat();
|
|
926
965
|
this.heartbeatInterval = setInterval(() => {
|
|
927
966
|
this.heartbeat();
|
|
928
967
|
}, HEARTBEAT_INTERVAL_MS);
|
|
929
968
|
this.heartbeatInterval.unref?.();
|
|
930
|
-
setInterval(() => {
|
|
931
|
-
this.
|
|
932
|
-
}, STALE_THRESHOLD_MS)
|
|
969
|
+
this.cleanupInterval = setInterval(() => {
|
|
970
|
+
this.cleanup();
|
|
971
|
+
}, STALE_THRESHOLD_MS);
|
|
972
|
+
this.cleanupInterval.unref?.();
|
|
933
973
|
this.registerMainProcess();
|
|
934
|
-
process.on("exit",
|
|
974
|
+
process.on("exit", this.onProcessExit);
|
|
935
975
|
}
|
|
936
976
|
/**
|
|
937
977
|
* Stop the heartbeat and clean up.
|
|
@@ -942,7 +982,12 @@ var PersistentProcessRegistry = class {
|
|
|
942
982
|
clearInterval(this.heartbeatInterval);
|
|
943
983
|
this.heartbeatInterval = null;
|
|
944
984
|
}
|
|
945
|
-
this.
|
|
985
|
+
if (this.cleanupInterval) {
|
|
986
|
+
clearInterval(this.cleanupInterval);
|
|
987
|
+
this.cleanupInterval = null;
|
|
988
|
+
}
|
|
989
|
+
process.off("exit", this.onProcessExit);
|
|
990
|
+
void this.syncToPersistent();
|
|
946
991
|
}
|
|
947
992
|
/**
|
|
948
993
|
* Register the main WrongStack process as protected.
|
|
@@ -1026,8 +1071,18 @@ var PersistentProcessRegistry = class {
|
|
|
1026
1071
|
* Send heartbeat to mark all this instance's processes as alive.
|
|
1027
1072
|
*/
|
|
1028
1073
|
heartbeat() {
|
|
1029
|
-
if (this.isShuttingDown) return;
|
|
1030
|
-
this.
|
|
1074
|
+
if (this.isShuttingDown || this.heartbeatRunning) return;
|
|
1075
|
+
this.heartbeatRunning = true;
|
|
1076
|
+
void this.syncToPersistent().finally(() => {
|
|
1077
|
+
this.heartbeatRunning = false;
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
cleanup() {
|
|
1081
|
+
if (this.isShuttingDown || this.cleanupRunning) return;
|
|
1082
|
+
this.cleanupRunning = true;
|
|
1083
|
+
void this.cleanupStaleEntries().finally(() => {
|
|
1084
|
+
this.cleanupRunning = false;
|
|
1085
|
+
});
|
|
1031
1086
|
}
|
|
1032
1087
|
/**
|
|
1033
1088
|
* Sync this instance's processes to the persistent registry.
|
|
@@ -1781,14 +1836,12 @@ var bashTool = {
|
|
|
1781
1836
|
const detached = !isWin2;
|
|
1782
1837
|
const startedAt = Date.now();
|
|
1783
1838
|
if (input.background) {
|
|
1784
|
-
let buf2 = "";
|
|
1785
|
-
let truncated = false;
|
|
1786
1839
|
const child2 = spawn2(shell, args, {
|
|
1787
1840
|
cwd: ctx.projectRoot,
|
|
1788
1841
|
env,
|
|
1789
1842
|
// PowerShell takes the script on stdin (no argv quoting); cmd.exe
|
|
1790
1843
|
// and POSIX shells ignore stdin when given the command inline.
|
|
1791
|
-
stdio: [plan.useStdin ? "pipe" : "ignore", "
|
|
1844
|
+
stdio: [plan.useStdin ? "pipe" : "ignore", "ignore", "ignore"],
|
|
1792
1845
|
// win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
|
|
1793
1846
|
// DETACHED_PROCESS (detached: true) is set, so the console-less
|
|
1794
1847
|
// cmd.exe's grandchildren (node, dev servers) each allocate a fresh
|
|
@@ -1807,8 +1860,8 @@ var bashTool = {
|
|
|
1807
1860
|
}
|
|
1808
1861
|
}
|
|
1809
1862
|
const pid2 = child2.pid;
|
|
1810
|
-
|
|
1811
|
-
|
|
1863
|
+
const stdoutBytes2 = 0;
|
|
1864
|
+
const stderrBytes2 = 0;
|
|
1812
1865
|
let telemetryCompleted2 = false;
|
|
1813
1866
|
emitProcessStarted({
|
|
1814
1867
|
...pid2 !== void 0 ? { pid: pid2 } : {},
|
|
@@ -1841,41 +1894,17 @@ var bashTool = {
|
|
|
1841
1894
|
startedAt: Date.now(),
|
|
1842
1895
|
sessionId: ctx.session?.id,
|
|
1843
1896
|
child: child2,
|
|
1844
|
-
processGroupLeader: detached && child2.pid === pid2
|
|
1897
|
+
processGroupLeader: detached && child2.pid === pid2,
|
|
1898
|
+
background: true
|
|
1845
1899
|
});
|
|
1846
1900
|
child2.on("close", () => registry.unregister(pid2));
|
|
1847
1901
|
}
|
|
1848
|
-
const onBgData = (chunk, stream) => {
|
|
1849
|
-
if (stream === "stdout") stdoutBytes2 += chunk.byteLength;
|
|
1850
|
-
else stderrBytes2 += chunk.byteLength;
|
|
1851
|
-
emitProcessOutput({ pid: pid2, stream, chunk });
|
|
1852
|
-
if (truncated) return;
|
|
1853
|
-
const remain = MAX_OUTPUT - buf2.length;
|
|
1854
|
-
if (remain > 0) {
|
|
1855
|
-
buf2 += chunk.toString().slice(0, remain);
|
|
1856
|
-
}
|
|
1857
|
-
if (buf2.length >= MAX_OUTPUT) {
|
|
1858
|
-
truncated = true;
|
|
1859
|
-
child2.stdout?.off("data", onBgStdout);
|
|
1860
|
-
child2.stderr?.off("data", onBgStderr);
|
|
1861
|
-
}
|
|
1862
|
-
};
|
|
1863
|
-
const onBgStdout = (chunk) => onBgData(chunk, "stdout");
|
|
1864
|
-
const onBgStderr = (chunk) => onBgData(chunk, "stderr");
|
|
1865
|
-
child2.stdout?.on("data", onBgStdout);
|
|
1866
|
-
child2.stderr?.on("data", onBgStderr);
|
|
1867
|
-
const cleanupBackground = () => {
|
|
1868
|
-
child2.stdout?.off("data", onBgStdout);
|
|
1869
|
-
child2.stderr?.off("data", onBgStderr);
|
|
1870
|
-
};
|
|
1871
1902
|
child2.on("error", () => {
|
|
1872
|
-
cleanupBackground();
|
|
1873
1903
|
if (typeof pid2 === "number") registry.unregister(pid2);
|
|
1874
1904
|
registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
|
|
1875
1905
|
completeBackground(1);
|
|
1876
1906
|
});
|
|
1877
1907
|
child2.on("close", (code, signal) => {
|
|
1878
|
-
cleanupBackground();
|
|
1879
1908
|
registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
|
|
1880
1909
|
completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
|
|
1881
1910
|
});
|
|
@@ -1883,7 +1912,7 @@ var bashTool = {
|
|
|
1883
1912
|
yield {
|
|
1884
1913
|
type: "final",
|
|
1885
1914
|
output: {
|
|
1886
|
-
output:
|
|
1915
|
+
output: "",
|
|
1887
1916
|
exit_code: null,
|
|
1888
1917
|
timed_out: false,
|
|
1889
1918
|
pid: pid2
|
|
@@ -2154,6 +2183,7 @@ ${hint}` : ""),
|
|
|
2154
2183
|
for (const entry of registry.bySession(sessionId)) {
|
|
2155
2184
|
if (entry.name !== "bash") continue;
|
|
2156
2185
|
if (entry.child.exitCode !== null) continue;
|
|
2186
|
+
if (entry.background) continue;
|
|
2157
2187
|
if (entry.protected) continue;
|
|
2158
2188
|
registry.kill(entry.pid, { force: true });
|
|
2159
2189
|
}
|