oh-my-opencode 5.0.0-beta.60 → 5.0.0-beta.62
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/cli/index.js +418 -330
- package/dist/cli-node/index.js +418 -330
- package/dist/index.js +14 -14
- package/package.json +14 -14
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +6885 -249
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +7 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +20 -11
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +20 -11
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +58 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +17 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +21 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.js +22 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +25 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +21 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-role-guard.ts +22 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +145 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +7 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-role-matrix.test.ts +113 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +3 -3
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +20 -11
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +3 -3
- package/packages/omo-codex/plugin/test/bootstrap-default-role.test.mjs +66 -0
- package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +13 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +7058 -417
- package/packages/prompts-core/prompts/ultrawork/codex.md +20 -11
|
@@ -3059,6 +3059,15 @@ var ULW_LOOP_MANIFEST = {
|
|
|
3059
3059
|
};
|
|
3060
3060
|
|
|
3061
3061
|
// components/ulw-loop/src/sdk/factory.ts
|
|
3062
|
+
function validateCodexGoalJson(raw) {
|
|
3063
|
+
if (raw === undefined)
|
|
3064
|
+
return;
|
|
3065
|
+
try {
|
|
3066
|
+
JSON.parse(raw);
|
|
3067
|
+
} catch (error) {
|
|
3068
|
+
throw new UlwLoopError(`Invalid codexGoal: ${error instanceof Error ? error.message : "not valid JSON"}`, "ULW_LOOP_CODEX_GOAL_JSON_INVALID", { cause: error });
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3062
3071
|
function validateContext(context) {
|
|
3063
3072
|
if (!context.cwd.trim())
|
|
3064
3073
|
throw new UlwLoopError("cwd is required.", "ULW_LOOP_CWD_REQUIRED");
|
|
@@ -3088,6 +3097,10 @@ function nextActionsFrom(result) {
|
|
|
3088
3097
|
return [];
|
|
3089
3098
|
return result.nextActions.filter((action) => typeof action === "string").slice(0, 8);
|
|
3090
3099
|
}
|
|
3100
|
+
function checkpointWithValidatedSnapshot(context, scope, args) {
|
|
3101
|
+
validateCodexGoalJson(args.codexGoalJson);
|
|
3102
|
+
return checkpointUlwLoop(context.cwd, args, scope, { surface: context.surface });
|
|
3103
|
+
}
|
|
3091
3104
|
function createAgentToolkit(context, deps = {}) {
|
|
3092
3105
|
validateContext(context);
|
|
3093
3106
|
const scope = { sessionId: context.sessionId };
|
|
@@ -3148,7 +3161,7 @@ function createAgentToolkit(context, deps = {}) {
|
|
|
3148
3161
|
};
|
|
3149
3162
|
}),
|
|
3150
3163
|
completeGoals: (args = {}) => invoke("complete-goals", () => startNextUlwLoop(context.cwd, args, scope)),
|
|
3151
|
-
checkpoint: (args) => invoke("checkpoint", () => args.printTemplate === true ? checkpointTemplate(context.cwd, scope, args.goalId, { surface: context.surface }) :
|
|
3164
|
+
checkpoint: (args) => invoke("checkpoint", () => args.printTemplate === true ? checkpointTemplate(context.cwd, scope, args.goalId, { surface: context.surface }) : checkpointWithValidatedSnapshot(context, scope, args)),
|
|
3152
3165
|
steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope)),
|
|
3153
3166
|
addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope)),
|
|
3154
3167
|
criteria: (args) => invoke("criteria", async () => {
|
|
@@ -3809,7 +3822,36 @@ function readAll(stdin) {
|
|
|
3809
3822
|
import { randomBytes } from "node:crypto";
|
|
3810
3823
|
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync, statSync as statSync3, writeFileSync } from "node:fs";
|
|
3811
3824
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
3812
|
-
|
|
3825
|
+
|
|
3826
|
+
// components/ulw-loop/src/spawn-role-guard.ts
|
|
3827
|
+
var LAZYCODEX_SPAWN_ROLES = new Set([
|
|
3828
|
+
"explorer",
|
|
3829
|
+
"lazycodex-clone-fidelity-reviewer",
|
|
3830
|
+
"lazycodex-code-reviewer",
|
|
3831
|
+
"lazycodex-gate-reviewer",
|
|
3832
|
+
"lazycodex-qa-executor",
|
|
3833
|
+
"lazycodex-worker-high",
|
|
3834
|
+
"lazycodex-worker-low",
|
|
3835
|
+
"lazycodex-worker-medium",
|
|
3836
|
+
"librarian",
|
|
3837
|
+
"metis",
|
|
3838
|
+
"momus",
|
|
3839
|
+
"plan"
|
|
3840
|
+
]);
|
|
3841
|
+
function spawnRoleDenial(input) {
|
|
3842
|
+
const role = typeof input === "object" && input !== null && "agent_type" in input ? input.agent_type : undefined;
|
|
3843
|
+
if (typeof role === "string" && LAZYCODEX_SPAWN_ROLES.has(role))
|
|
3844
|
+
return null;
|
|
3845
|
+
return `LazyCodex requires an explicit registered agent_type: ${[...LAZYCODEX_SPAWN_ROLES].join(", ")}. Received ${JSON.stringify(role) ?? "no agent_type"}. Use the matching role and fork_turns: "none" (V2) or fork_context: false (V1), unless full history is deliberately required. The hook cannot see the tool schema; if agent_type is unavailable, stop and report incompatible role routing rather than spawning a generic agent. Describing a role in message does not select its TOML.`;
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
// components/ulw-loop/src/spawn-guard.ts
|
|
3849
|
+
var SPAWN_TOOL_TOKENS = new Set([
|
|
3850
|
+
"spawn_agent",
|
|
3851
|
+
"multi_agent_v1.spawn_agent",
|
|
3852
|
+
"collaborationspawn_agent",
|
|
3853
|
+
"collaboration.spawn_agent"
|
|
3854
|
+
]);
|
|
3813
3855
|
var DEFAULT_FANOUT_LIMIT = 24;
|
|
3814
3856
|
var DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
3815
3857
|
var GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
@@ -3820,6 +3862,16 @@ var REVIEW_AGENT_TYPES = [
|
|
|
3820
3862
|
];
|
|
3821
3863
|
var REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
|
|
3822
3864
|
function applySpawnGuards(payload, options = {}) {
|
|
3865
|
+
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
3866
|
+
return "";
|
|
3867
|
+
if (resolveToolkitSurface() === "lazycodex") {
|
|
3868
|
+
const reason = spawnRoleDenial(payload.tool_input);
|
|
3869
|
+
if (reason !== null)
|
|
3870
|
+
return deny(reason);
|
|
3871
|
+
}
|
|
3872
|
+
return applySpawnBudgetGuards(payload, options);
|
|
3873
|
+
}
|
|
3874
|
+
function applySpawnBudgetGuards(payload, options = {}) {
|
|
3823
3875
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
3824
3876
|
return "";
|
|
3825
3877
|
const breaker = readAdmissionBreaker(payload.session_id);
|
|
@@ -3886,14 +3938,15 @@ async function runSpawnGuardCli(stdin, stdout) {
|
|
|
3886
3938
|
for await (const chunk of stdin)
|
|
3887
3939
|
chunks.push(Buffer.from(chunk));
|
|
3888
3940
|
const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
|
|
3889
|
-
if (payload === null)
|
|
3941
|
+
if (payload === null) {
|
|
3942
|
+
stdout.write(deny("LazyCodex spawn guard received an invalid hook payload; role routing was not verified."));
|
|
3890
3943
|
return;
|
|
3944
|
+
}
|
|
3891
3945
|
const output = applySpawnGuards(payload);
|
|
3892
3946
|
if (output.length > 0)
|
|
3893
3947
|
stdout.write(output);
|
|
3894
3948
|
} catch (error) {
|
|
3895
|
-
|
|
3896
|
-
return;
|
|
3949
|
+
stdout.write(deny(`LazyCodex spawn guard failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
3897
3950
|
}
|
|
3898
3951
|
}
|
|
3899
3952
|
function peekFanOutBudget(stateDir) {
|
|
@@ -9,6 +9,18 @@ import { UlwLoopError } from "../runtime.js";
|
|
|
9
9
|
import { statusNextActions } from "../status-next-actions.js";
|
|
10
10
|
import { steerUlwLoop } from "../steering.js";
|
|
11
11
|
import { ULW_LOOP_MANIFEST } from "./manifest.js";
|
|
12
|
+
// The SDK takes inline values only, so a snapshot that is not JSON is a caller error - normalize it
|
|
13
|
+
// to the same stable code the CLI reports instead of letting it surface as a generic failure.
|
|
14
|
+
function validateCodexGoalJson(raw) {
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new UlwLoopError(`Invalid codexGoal: ${error instanceof Error ? error.message : "not valid JSON"}`, "ULW_LOOP_CODEX_GOAL_JSON_INVALID", { cause: error });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
12
24
|
function validateContext(context) {
|
|
13
25
|
if (!context.cwd.trim())
|
|
14
26
|
throw new UlwLoopError("cwd is required.", "ULW_LOOP_CWD_REQUIRED");
|
|
@@ -42,6 +54,10 @@ function nextActionsFrom(result) {
|
|
|
42
54
|
return [];
|
|
43
55
|
return result.nextActions.filter((action) => typeof action === "string").slice(0, 8);
|
|
44
56
|
}
|
|
57
|
+
function checkpointWithValidatedSnapshot(context, scope, args) {
|
|
58
|
+
validateCodexGoalJson(args.codexGoalJson);
|
|
59
|
+
return checkpointUlwLoop(context.cwd, args, scope, { surface: context.surface });
|
|
60
|
+
}
|
|
45
61
|
export function createAgentToolkit(context, deps = {}) {
|
|
46
62
|
validateContext(context);
|
|
47
63
|
const scope = { sessionId: context.sessionId };
|
|
@@ -108,7 +124,7 @@ export function createAgentToolkit(context, deps = {}) {
|
|
|
108
124
|
completeGoals: (args = {}) => invoke("complete-goals", () => startNextUlwLoop(context.cwd, args, scope)),
|
|
109
125
|
checkpoint: (args) => invoke("checkpoint", () => args.printTemplate === true
|
|
110
126
|
? checkpointTemplate(context.cwd, scope, args.goalId, { surface: context.surface })
|
|
111
|
-
:
|
|
127
|
+
: checkpointWithValidatedSnapshot(context, scope, args)),
|
|
112
128
|
steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope)),
|
|
113
129
|
addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope)),
|
|
114
130
|
criteria: (args) => invoke("criteria", async () => {
|
|
@@ -4,5 +4,6 @@ export interface SpawnGuardOptions {
|
|
|
4
4
|
readonly lockTimeoutMs?: number;
|
|
5
5
|
}
|
|
6
6
|
export declare function applySpawnGuards(payload: PreToolUsePayload, options?: SpawnGuardOptions): string;
|
|
7
|
+
export declare function applySpawnBudgetGuards(payload: PreToolUsePayload, options?: SpawnGuardOptions): string;
|
|
7
8
|
export declare function runSpawnAdmissionRecorderCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void>;
|
|
8
9
|
export declare function runSpawnGuardCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void>;
|
|
@@ -4,12 +4,18 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
5
5
|
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
6
6
|
import { ulwLoopAttemptEvidenceDir, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
|
|
7
|
+
import { spawnRoleDenial } from "./spawn-role-guard.js";
|
|
7
8
|
import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
|
|
8
9
|
import { GATE_REVIEWER_AGENT_NAMES, REVIEWER_ROLES_BY_SURFACE, resolveToolkitSurface, reviewerRolesFor, } from "./surface.js";
|
|
9
10
|
// spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
|
|
10
11
|
// hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
|
|
11
12
|
// dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
|
|
12
|
-
const SPAWN_TOOL_TOKENS = new Set([
|
|
13
|
+
const SPAWN_TOOL_TOKENS = new Set([
|
|
14
|
+
"spawn_agent",
|
|
15
|
+
"multi_agent_v1.spawn_agent",
|
|
16
|
+
"collaborationspawn_agent",
|
|
17
|
+
"collaboration.spawn_agent",
|
|
18
|
+
]);
|
|
13
19
|
export const DEFAULT_FANOUT_LIMIT = 24;
|
|
14
20
|
const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
15
21
|
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
@@ -20,6 +26,16 @@ const REVIEW_AGENT_TYPES = [
|
|
|
20
26
|
];
|
|
21
27
|
const REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
|
|
22
28
|
export function applySpawnGuards(payload, options = {}) {
|
|
29
|
+
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
30
|
+
return "";
|
|
31
|
+
if (resolveToolkitSurface() === "lazycodex") {
|
|
32
|
+
const reason = spawnRoleDenial(payload.tool_input);
|
|
33
|
+
if (reason !== null)
|
|
34
|
+
return deny(reason);
|
|
35
|
+
}
|
|
36
|
+
return applySpawnBudgetGuards(payload, options);
|
|
37
|
+
}
|
|
38
|
+
export function applySpawnBudgetGuards(payload, options = {}) {
|
|
23
39
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
24
40
|
return "";
|
|
25
41
|
const breaker = readAdmissionBreaker(payload.session_id);
|
|
@@ -93,15 +109,16 @@ export async function runSpawnGuardCli(stdin, stdout) {
|
|
|
93
109
|
for await (const chunk of stdin)
|
|
94
110
|
chunks.push(Buffer.from(chunk));
|
|
95
111
|
const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
|
|
96
|
-
if (payload === null)
|
|
112
|
+
if (payload === null) {
|
|
113
|
+
stdout.write(deny("LazyCodex spawn guard received an invalid hook payload; role routing was not verified."));
|
|
97
114
|
return;
|
|
115
|
+
}
|
|
98
116
|
const output = applySpawnGuards(payload);
|
|
99
117
|
if (output.length > 0)
|
|
100
118
|
stdout.write(output);
|
|
101
119
|
}
|
|
102
120
|
catch (error) {
|
|
103
|
-
|
|
104
|
-
return;
|
|
121
|
+
stdout.write(deny(`LazyCodex spawn guard failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
105
122
|
}
|
|
106
123
|
}
|
|
107
124
|
// Read-only fan-out eligibility check. Returns a denial reason when the next
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Explicit selectors, pinned by a test to the bundled role TOMLs. The generated
|
|
2
|
+
// default is a non-fork fallback, not a selector: a user can opt out of owning it.
|
|
3
|
+
export const LAZYCODEX_SPAWN_ROLES = new Set([
|
|
4
|
+
"explorer",
|
|
5
|
+
"lazycodex-clone-fidelity-reviewer",
|
|
6
|
+
"lazycodex-code-reviewer",
|
|
7
|
+
"lazycodex-gate-reviewer",
|
|
8
|
+
"lazycodex-qa-executor",
|
|
9
|
+
"lazycodex-worker-high",
|
|
10
|
+
"lazycodex-worker-low",
|
|
11
|
+
"lazycodex-worker-medium",
|
|
12
|
+
"librarian",
|
|
13
|
+
"metis",
|
|
14
|
+
"momus",
|
|
15
|
+
"plan",
|
|
16
|
+
]);
|
|
17
|
+
export function spawnRoleDenial(input) {
|
|
18
|
+
const role = typeof input === "object" && input !== null && "agent_type" in input ? input.agent_type : undefined;
|
|
19
|
+
if (typeof role === "string" && LAZYCODEX_SPAWN_ROLES.has(role))
|
|
20
|
+
return null;
|
|
21
|
+
return `LazyCodex requires an explicit registered agent_type: ${[...LAZYCODEX_SPAWN_ROLES].join(", ")}. Received ${JSON.stringify(role) ?? "no agent_type"}. Use the matching role and fork_turns: "none" (V2) or fork_context: false (V1), unless full history is deliberately required. The hook cannot see the tool schema; if agent_type is unavailable, stop and report incompatible role routing rather than spawning a generic agent. Describing a role in message does not select its TOML.`;
|
|
22
|
+
}
|
|
@@ -7,20 +7,20 @@
|
|
|
7
7
|
"type": "command",
|
|
8
8
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
|
|
9
9
|
"timeout": 10,
|
|
10
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
10
|
+
"statusMessage": "(OmO 5.0.0-beta.62) Checking Ulw-Loop Steering"
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"PostToolUse": [
|
|
16
16
|
{
|
|
17
|
-
"matcher": "^(spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
|
|
17
|
+
"matcher": "^(spawn_agent|multi_agent_v1\\.spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
|
|
18
18
|
"hooks": [
|
|
19
19
|
{
|
|
20
20
|
"type": "command",
|
|
21
21
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use-spawn",
|
|
22
22
|
"timeout": 5,
|
|
23
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
23
|
+
"statusMessage": "(OmO 5.0.0-beta.62) Recording Spawn Admission"
|
|
24
24
|
}
|
|
25
25
|
]
|
|
26
26
|
}
|
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
"type": "command",
|
|
34
34
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
|
|
35
35
|
"timeout": 5,
|
|
36
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
36
|
+
"statusMessage": "(OmO 5.0.0-beta.62) Enforcing Unlimited Ulw-Loop Budget"
|
|
37
37
|
}
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
|
-
"matcher": "^(spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
|
|
41
|
+
"matcher": "^(spawn_agent|multi_agent_v1\\.spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
|
|
42
42
|
"hooks": [
|
|
43
43
|
{
|
|
44
44
|
"type": "command",
|
|
45
45
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
|
|
46
46
|
"timeout": 5,
|
|
47
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
47
|
+
"statusMessage": "(OmO 5.0.0-beta.62) Guarding Ulw-Loop Spawns"
|
|
48
48
|
}
|
|
49
49
|
]
|
|
50
50
|
}
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"type": "command",
|
|
57
57
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
|
|
58
58
|
"timeout": 10,
|
|
59
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
59
|
+
"statusMessage": "(OmO 5.0.0-beta.62) Checking Ulw-Loop Resume"
|
|
60
60
|
}
|
|
61
61
|
]
|
|
62
62
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/codex-ulw-loop",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.62",
|
|
4
4
|
"description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.12.1",
|
|
@@ -24,6 +24,21 @@ import type {
|
|
|
24
24
|
ToolkitUnknownRequest,
|
|
25
25
|
} from "./types.js";
|
|
26
26
|
|
|
27
|
+
// The SDK takes inline values only, so a snapshot that is not JSON is a caller error - normalize it
|
|
28
|
+
// to the same stable code the CLI reports instead of letting it surface as a generic failure.
|
|
29
|
+
function validateCodexGoalJson(raw: string | undefined): void {
|
|
30
|
+
if (raw === undefined) return;
|
|
31
|
+
try {
|
|
32
|
+
JSON.parse(raw);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new UlwLoopError(
|
|
35
|
+
`Invalid codexGoal: ${error instanceof Error ? error.message : "not valid JSON"}`,
|
|
36
|
+
"ULW_LOOP_CODEX_GOAL_JSON_INVALID",
|
|
37
|
+
{ cause: error },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
function validateContext(context: ToolkitContext): void {
|
|
28
43
|
if (!context.cwd.trim()) throw new UlwLoopError("cwd is required.", "ULW_LOOP_CWD_REQUIRED");
|
|
29
44
|
if (!context.sessionId.trim())
|
|
@@ -63,6 +78,15 @@ function nextActionsFrom(result: object): readonly string[] {
|
|
|
63
78
|
return result.nextActions.filter((action): action is string => typeof action === "string").slice(0, 8);
|
|
64
79
|
}
|
|
65
80
|
|
|
81
|
+
function checkpointWithValidatedSnapshot(
|
|
82
|
+
context: ToolkitContext,
|
|
83
|
+
scope: UlwLoopScope,
|
|
84
|
+
args: Exclude<CheckpointArgs, { readonly printTemplate: true }>,
|
|
85
|
+
): ReturnType<typeof checkpointUlwLoop> {
|
|
86
|
+
validateCodexGoalJson(args.codexGoalJson);
|
|
87
|
+
return checkpointUlwLoop(context.cwd, args, scope, { surface: context.surface });
|
|
88
|
+
}
|
|
89
|
+
|
|
66
90
|
export function createAgentToolkit(context: ToolkitContext, deps: AgentToolkitDependencies = {}): AgentToolkit {
|
|
67
91
|
validateContext(context);
|
|
68
92
|
const scope: UlwLoopScope = { sessionId: context.sessionId };
|
|
@@ -139,7 +163,7 @@ export function createAgentToolkit(context: ToolkitContext, deps: AgentToolkitDe
|
|
|
139
163
|
invoke("checkpoint", () =>
|
|
140
164
|
args.printTemplate === true
|
|
141
165
|
? checkpointTemplate(context.cwd, scope, args.goalId, { surface: context.surface })
|
|
142
|
-
:
|
|
166
|
+
: checkpointWithValidatedSnapshot(context, scope, args),
|
|
143
167
|
),
|
|
144
168
|
steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope)),
|
|
145
169
|
addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope)),
|
|
@@ -6,6 +6,7 @@ import type { PreToolUsePayload } from "./codex-hook.js";
|
|
|
6
6
|
import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
7
7
|
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
8
8
|
import { ulwLoopAttemptEvidenceDir, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
|
|
9
|
+
import { spawnRoleDenial } from "./spawn-role-guard.js";
|
|
9
10
|
import { isStateLockTimeout, type StateLockOptions, withStateLockSync } from "./state-lock.js";
|
|
10
11
|
import {
|
|
11
12
|
GATE_REVIEWER_AGENT_NAMES,
|
|
@@ -18,7 +19,12 @@ import type { UlwLoopPlan } from "./types.js";
|
|
|
18
19
|
// spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
|
|
19
20
|
// hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
|
|
20
21
|
// dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
|
|
21
|
-
const SPAWN_TOOL_TOKENS = new Set([
|
|
22
|
+
const SPAWN_TOOL_TOKENS = new Set([
|
|
23
|
+
"spawn_agent",
|
|
24
|
+
"multi_agent_v1.spawn_agent",
|
|
25
|
+
"collaborationspawn_agent",
|
|
26
|
+
"collaboration.spawn_agent",
|
|
27
|
+
]);
|
|
22
28
|
export const DEFAULT_FANOUT_LIMIT = 24;
|
|
23
29
|
const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
24
30
|
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
@@ -34,6 +40,15 @@ export interface SpawnGuardOptions {
|
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export function applySpawnGuards(payload: PreToolUsePayload, options: SpawnGuardOptions = {}): string {
|
|
43
|
+
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
|
|
44
|
+
if (resolveToolkitSurface() === "lazycodex") {
|
|
45
|
+
const reason = spawnRoleDenial(payload.tool_input);
|
|
46
|
+
if (reason !== null) return deny(reason);
|
|
47
|
+
}
|
|
48
|
+
return applySpawnBudgetGuards(payload, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function applySpawnBudgetGuards(payload: PreToolUsePayload, options: SpawnGuardOptions = {}): string {
|
|
37
52
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
|
|
38
53
|
const breaker = readAdmissionBreaker(payload.session_id);
|
|
39
54
|
if (breaker !== null)
|
|
@@ -110,11 +125,14 @@ export async function runSpawnGuardCli(stdin: NodeJS.ReadableStream, stdout: Nod
|
|
|
110
125
|
const chunks: Buffer[] = [];
|
|
111
126
|
for await (const chunk of stdin) chunks.push(Buffer.from(chunk));
|
|
112
127
|
const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
|
|
113
|
-
if (payload === null)
|
|
128
|
+
if (payload === null) {
|
|
129
|
+
stdout.write(deny("LazyCodex spawn guard received an invalid hook payload; role routing was not verified."));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
114
132
|
const output = applySpawnGuards(payload);
|
|
115
133
|
if (output.length > 0) stdout.write(output);
|
|
116
134
|
} catch (error) {
|
|
117
|
-
|
|
135
|
+
stdout.write(deny(`LazyCodex spawn guard failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
118
136
|
}
|
|
119
137
|
}
|
|
120
138
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Explicit selectors, pinned by a test to the bundled role TOMLs. The generated
|
|
2
|
+
// default is a non-fork fallback, not a selector: a user can opt out of owning it.
|
|
3
|
+
export const LAZYCODEX_SPAWN_ROLES: ReadonlySet<string> = new Set([
|
|
4
|
+
"explorer",
|
|
5
|
+
"lazycodex-clone-fidelity-reviewer",
|
|
6
|
+
"lazycodex-code-reviewer",
|
|
7
|
+
"lazycodex-gate-reviewer",
|
|
8
|
+
"lazycodex-qa-executor",
|
|
9
|
+
"lazycodex-worker-high",
|
|
10
|
+
"lazycodex-worker-low",
|
|
11
|
+
"lazycodex-worker-medium",
|
|
12
|
+
"librarian",
|
|
13
|
+
"metis",
|
|
14
|
+
"momus",
|
|
15
|
+
"plan",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function spawnRoleDenial(input: unknown): string | null {
|
|
19
|
+
const role = typeof input === "object" && input !== null && "agent_type" in input ? input.agent_type : undefined;
|
|
20
|
+
if (typeof role === "string" && LAZYCODEX_SPAWN_ROLES.has(role)) return null;
|
|
21
|
+
return `LazyCodex requires an explicit registered agent_type: ${[...LAZYCODEX_SPAWN_ROLES].join(", ")}. Received ${JSON.stringify(role) ?? "no agent_type"}. Use the matching role and fork_turns: "none" (V2) or fork_context: false (V1), unless full history is deliberately required. The hook cannot see the tool schema; if agent_type is unavailable, stop and report incompatible role routing rather than spawning a generic agent. Describing a role in message does not select its TOML.`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { readUlwLoopPlan } from "../src/plan-io.js";
|
|
7
|
+
import { createAgentToolkit } from "../src/sdk.js";
|
|
8
|
+
|
|
9
|
+
const workDirs: string[] = [];
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
for (const dir of workDirs.splice(0)) await rm(dir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
interface Seeded {
|
|
16
|
+
readonly cwd: string;
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly goalId: string;
|
|
19
|
+
readonly objective: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// One fixture shape for every driver state: a real plan whose first goal is ready to close, so the
|
|
23
|
+
// only thing under test is what the driver snapshot does to the checkpoint.
|
|
24
|
+
async function seed(sessionId: string): Promise<Seeded> {
|
|
25
|
+
const cwd = await mkdtemp(join(tmpdir(), "ulw-driver-"));
|
|
26
|
+
workDirs.push(cwd);
|
|
27
|
+
const toolkit = createAgentToolkit({ cwd, sessionId, surface: "omo-senpi" });
|
|
28
|
+
const created = await toolkit.createGoals({ brief: "- alpha goal\n- beta goal" });
|
|
29
|
+
if (!created.ok) throw new Error("fixture could not create goals");
|
|
30
|
+
const started = await toolkit.completeGoals({});
|
|
31
|
+
if (!started.ok || started.operation !== "complete-goals" || "done" in started.result)
|
|
32
|
+
throw new Error("fixture could not start a goal");
|
|
33
|
+
const goalId = started.result.goal.id;
|
|
34
|
+
const criteria = await toolkit.criteria({ goalId });
|
|
35
|
+
if (!criteria.ok || criteria.operation !== "criteria") throw new Error("fixture has no criteria");
|
|
36
|
+
for (const criterion of criteria.result.criteria) {
|
|
37
|
+
const recorded = await toolkit.recordEvidence({
|
|
38
|
+
goalId,
|
|
39
|
+
criterionId: criterion.id,
|
|
40
|
+
status: "pass",
|
|
41
|
+
evidence: "driver-state fixture proof",
|
|
42
|
+
});
|
|
43
|
+
if (!recorded.ok) throw new Error("fixture could not record evidence");
|
|
44
|
+
}
|
|
45
|
+
const plan = await readUlwLoopPlan(cwd, { sessionId });
|
|
46
|
+
return { cwd, sessionId, goalId, objective: plan.codexObjective ?? "" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function closeWith(seeded: Seeded, snapshot?: Record<string, unknown>) {
|
|
50
|
+
const toolkit = createAgentToolkit({ cwd: seeded.cwd, sessionId: seeded.sessionId, surface: "omo-senpi" });
|
|
51
|
+
return toolkit.checkpoint({
|
|
52
|
+
goalId: seeded.goalId,
|
|
53
|
+
status: "complete",
|
|
54
|
+
evidence: "driver-state checkpoint",
|
|
55
|
+
...(snapshot === undefined ? {} : { codexGoalJson: JSON.stringify(snapshot) }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("SDK driver-goal states", () => {
|
|
60
|
+
describe("#given no driver snapshot", () => {
|
|
61
|
+
it("#when the goal is checkpointed #then it completes and advises creating the driver goal", async () => {
|
|
62
|
+
const seeded = await seed("driver-absent");
|
|
63
|
+
|
|
64
|
+
const closed = await closeWith(seeded);
|
|
65
|
+
|
|
66
|
+
expect(closed.ok).toBe(true);
|
|
67
|
+
if (closed.ok) expect(closed.nextActions.join(" ")).toContain("create_goal");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("#given an active driver carrying the plan objective", () => {
|
|
72
|
+
it("#when the goal is checkpointed #then it completes with no objective difference reported", async () => {
|
|
73
|
+
const seeded = await seed("driver-active");
|
|
74
|
+
|
|
75
|
+
const closed = await closeWith(seeded, { goal: { objective: seeded.objective, status: "active" } });
|
|
76
|
+
|
|
77
|
+
expect(closed.ok).toBe(true);
|
|
78
|
+
if (closed.ok) {
|
|
79
|
+
expect([...(closed.warnings ?? []), ...closed.nextActions].join(" ")).not.toContain(
|
|
80
|
+
"driver_objective_differs",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("#given a driver completed before the plan finished", () => {
|
|
87
|
+
it("#when the goal is checkpointed #then it completes and advises re-creating the driver", async () => {
|
|
88
|
+
const seeded = await seed("driver-early");
|
|
89
|
+
|
|
90
|
+
const closed = await closeWith(seeded, { goal: { objective: seeded.objective, status: "complete" } });
|
|
91
|
+
|
|
92
|
+
expect(closed.ok).toBe(true);
|
|
93
|
+
if (closed.ok) expect(closed.nextActions.join(" ")).toContain("create_goal");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("#given a driver the harness has limited", () => {
|
|
98
|
+
for (const status of ["paused", "usage_limited", "budget_limited"] as const) {
|
|
99
|
+
it(`#when the driver is ${status} #then the checkpoint still completes and advises resuming it`, async () => {
|
|
100
|
+
const seeded = await seed(`driver-${status}`);
|
|
101
|
+
|
|
102
|
+
const closed = await closeWith(seeded, { goal: { objective: seeded.objective, status } });
|
|
103
|
+
|
|
104
|
+
expect(closed.ok).toBe(true);
|
|
105
|
+
if (closed.ok) expect(closed.nextActions.join(" ")).toContain("/goal resume");
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("#given a driver whose objective differs from the plan", () => {
|
|
111
|
+
it("#when the goal is checkpointed #then the difference is reported, never a rejection", async () => {
|
|
112
|
+
const seeded = await seed("driver-differs");
|
|
113
|
+
|
|
114
|
+
const closed = await closeWith(seeded, { goal: { objective: "a different objective", status: "active" } });
|
|
115
|
+
|
|
116
|
+
expect(closed.ok).toBe(true);
|
|
117
|
+
if (closed.ok) {
|
|
118
|
+
expect([...(closed.warnings ?? []), ...closed.nextActions].join(" ")).toContain("driver_objective_differs");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("#given snapshot input that is neither JSON nor a readable path", () => {
|
|
124
|
+
it("#when the goal is checkpointed #then the typed error is returned and the goal stays open", async () => {
|
|
125
|
+
const seeded = await seed("driver-malformed");
|
|
126
|
+
|
|
127
|
+
const closed = await closeWith(seeded);
|
|
128
|
+
expect(closed.ok).toBe(true);
|
|
129
|
+
|
|
130
|
+
const second = await seed("driver-malformed-2");
|
|
131
|
+
const toolkit = createAgentToolkit({ cwd: second.cwd, sessionId: second.sessionId, surface: "omo-senpi" });
|
|
132
|
+
const rejected = await toolkit.checkpoint({
|
|
133
|
+
goalId: second.goalId,
|
|
134
|
+
status: "complete",
|
|
135
|
+
evidence: "driver-state checkpoint",
|
|
136
|
+
codexGoalJson: "not json and not a path",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(rejected.ok).toBe(false);
|
|
140
|
+
if (!rejected.ok) expect(rejected.error.code).toBe("ULW_LOOP_CODEX_GOAL_JSON_INVALID");
|
|
141
|
+
const plan = await readUlwLoopPlan(second.cwd, { sessionId: second.sessionId });
|
|
142
|
+
expect(plan.goals.find((goal) => goal.id === second.goalId)?.status).not.toBe("complete");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -6,7 +6,13 @@ import { Readable, Writable } from "node:stream";
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
|
|
8
8
|
import type { PreToolUsePayload } from "../src/codex-hook.ts";
|
|
9
|
-
|
|
9
|
+
// Budget/artifact behavior is independent of role admission; the native entry
|
|
10
|
+
// point and its composition are covered in spawn-role-matrix.test.ts.
|
|
11
|
+
import {
|
|
12
|
+
applySpawnBudgetGuards as applySpawnGuards,
|
|
13
|
+
DEFAULT_FANOUT_LIMIT,
|
|
14
|
+
runSpawnAdmissionRecorderCli,
|
|
15
|
+
} from "../src/spawn-guard.ts";
|
|
10
16
|
|
|
11
17
|
let workDir: string;
|
|
12
18
|
let originalLimit: string | undefined;
|